@openmoq/playback 0.0.0-stage → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE.txt +202 -0
  2. package/README.md +18 -2
  3. package/dist/adaptive-tolerance.d.ts +128 -0
  4. package/dist/adaptive-tolerance.d.ts.map +1 -0
  5. package/dist/adaptive-tolerance.js +339 -0
  6. package/dist/adaptive-tolerance.js.map +1 -0
  7. package/dist/bandwidth-estimator.d.ts +49 -0
  8. package/dist/bandwidth-estimator.d.ts.map +1 -0
  9. package/dist/bandwidth-estimator.js +77 -0
  10. package/dist/bandwidth-estimator.js.map +1 -0
  11. package/dist/browser/index.global.js +2813 -0
  12. package/dist/browser/index.global.js.map +1 -0
  13. package/dist/browser/index.js +2788 -0
  14. package/dist/browser/index.js.map +1 -0
  15. package/dist/buffer-based-controller.d.ts +87 -0
  16. package/dist/buffer-based-controller.d.ts.map +1 -0
  17. package/dist/buffer-based-controller.js +85 -0
  18. package/dist/buffer-based-controller.js.map +1 -0
  19. package/dist/decoder-state.d.ts +103 -0
  20. package/dist/decoder-state.d.ts.map +1 -0
  21. package/dist/decoder-state.js +145 -0
  22. package/dist/decoder-state.js.map +1 -0
  23. package/dist/gap-detector.d.ts +83 -0
  24. package/dist/gap-detector.d.ts.map +1 -0
  25. package/dist/gap-detector.js +129 -0
  26. package/dist/gap-detector.js.map +1 -0
  27. package/dist/index.d.ts +33 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +33 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/jitter-buffer.d.ts +73 -0
  32. package/dist/jitter-buffer.d.ts.map +1 -0
  33. package/dist/jitter-buffer.js +216 -0
  34. package/dist/jitter-buffer.js.map +1 -0
  35. package/dist/keyframe-validator.d.ts +26 -0
  36. package/dist/keyframe-validator.d.ts.map +1 -0
  37. package/dist/keyframe-validator.js +170 -0
  38. package/dist/keyframe-validator.js.map +1 -0
  39. package/dist/pipeline.d.ts +215 -0
  40. package/dist/pipeline.d.ts.map +1 -0
  41. package/dist/pipeline.js +852 -0
  42. package/dist/pipeline.js.map +1 -0
  43. package/dist/recovery.d.ts +166 -0
  44. package/dist/recovery.d.ts.map +1 -0
  45. package/dist/recovery.js +176 -0
  46. package/dist/recovery.js.map +1 -0
  47. package/dist/render-scheduler.d.ts +50 -0
  48. package/dist/render-scheduler.d.ts.map +1 -0
  49. package/dist/render-scheduler.js +84 -0
  50. package/dist/render-scheduler.js.map +1 -0
  51. package/dist/startup-buffer.d.ts +68 -0
  52. package/dist/startup-buffer.d.ts.map +1 -0
  53. package/dist/startup-buffer.js +107 -0
  54. package/dist/startup-buffer.js.map +1 -0
  55. package/dist/sync.d.ts +234 -0
  56. package/dist/sync.d.ts.map +1 -0
  57. package/dist/sync.js +343 -0
  58. package/dist/sync.js.map +1 -0
  59. package/dist/types.d.ts +229 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +12 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +56 -3
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/index.ts","../../src/jitter-buffer.ts","../../src/gap-detector.ts","../../src/decoder-state.ts","../../src/sync.ts","../../src/recovery.ts","../../src/adaptive-tolerance.ts","../../src/render-scheduler.ts","../../src/startup-buffer.ts","../../src/keyframe-validator.ts","../../../transport/src/primitives/varint.ts","../../../transport/src/control/codes-18.ts","../../../transport/src/errors.ts","../../../transport/src/primitives/bytes.ts","../../../transport/src/control/message-params-18.ts","../../../transport/src/primitives/reason.ts","../../../transport/src/control/codes.ts","../../../transport/src/control/parameters.ts","../../../transport/src/control/encoder.ts","../../../transport/src/control/decoder.ts","../../../transport/src/control/draft14-codec.ts","../../../transport/src/control/draft18-codec.ts","../../../transport/src/data/codes.ts","../../../transport/src/data/decoder.ts","../../../transport/src/qlog/trace.ts","../../../transport/src/control/auth-token.ts","../../../transport/src/session/setup.ts","../../../transport/src/session/session.ts","../../../loc/src/types.ts","../../../loc/src/property-map.ts","../../../loc/src/headers.ts","../../src/pipeline.ts","../../src/bandwidth-estimator.ts","../../src/buffer-based-controller.ts"],"sourcesContent":["/**\n * @openmoq/playback — Sans-I/O playback core.\n *\n * Jitter buffer, A/V sync, gap detection, decoder state machine,\n * and pipeline orchestrator — all testable in Node.js without browser APIs.\n *\n * @see draft-ietf-moq-loc-01 §4 (Application Mapping)\n * @see draft-ietf-moq-transport-16 §10 (Objects)\n * @module\n */\n\n// ─── Types ───────────────────────────────────────────────────────────\n\nexport type {\n ClockSource,\n DecoderCommand,\n ConfigureCommand,\n DecodeVideoCommand,\n DecodeAudioCommand,\n FlushCommand,\n ResetCommand,\n SetPlaybackRateCommand,\n PlaybackEvent,\n GapDetectedEvent,\n SkipForwardEvent,\n SyncDriftEvent,\n KeyframeWaitingEvent,\n KeyframeValidationFailedEvent,\n TrackEndedEvent,\n RecoveryEvent,\n CatchUpState,\n CatchUpChangedEvent,\n BacklogShedEvent,\n PlaybackConfig,\n DecoderFeedback,\n QueuePressureFeedback,\n DecodeErrorFeedback,\n FrameRenderedFeedback,\n FlushCompleteFeedback,\n} from './types.js';\n\n// ─── Jitter Buffer ──────────────────────────────────────────────────\n\nexport { JitterBuffer } from './jitter-buffer.js';\n\n// ─── Gap Detector ───────────────────────────────────────────────────\n\nexport { GapDetector, GapAction } from './gap-detector.js';\nexport type { GapActionValue, GapDecision } from './gap-detector.js';\n\n// ─── Decoder State ──────────────────────────────────────────────────\n\nexport { DecoderStateMachine, DecoderState } from './decoder-state.js';\nexport type { DecoderStateValue, FrameDecision } from './decoder-state.js';\n\n// ─── Sync Controller ───────────────────────────────────────────────\n\nexport { SyncController } from './sync.js';\nexport type { RenderTiming, WallClockSource, SyncControllerConfig } from './sync.js';\n\n// ─── Recovery ───────────────────────────────────────────────────────\n\nexport { DefaultRecoveryController } from './recovery.js';\nexport type { DefaultRecoveryConfig, RecoveryController, RecoveryTrigger, RecoveryAction } from './recovery.js';\n\n// ─── Adaptive Tolerance ─────────────────────────────────────────────\n\nexport { AdaptiveToleranceController, RecoveryPhase, DEFAULT_TOLERANCE_CONFIG } from './adaptive-tolerance.js';\nexport type { ToleranceConfig, TickResult } from './adaptive-tolerance.js';\n\n// ─── Render Scheduler ───────────────────────────────────────────────\n\nexport { RenderScheduler } from './render-scheduler.js';\nexport type { RenderSchedulerOptions } from './render-scheduler.js';\n\n// ─── Startup Buffer ─────────────────────────────────────────────────\n\nexport { StartupBuffer, classifyNetwork } from './startup-buffer.js';\nexport type { StartupParams } from './startup-buffer.js';\n\n// ─── Keyframe Validator ─────────────────────────────────────────────\n\nexport { isKeyframePayload } from './keyframe-validator.js';\n\n// ─── Pipeline ───────────────────────────────────────────────────────\n\nexport { PlaybackPipeline } from './pipeline.js';\nexport type { PipelineOptions } from './pipeline.js';\n\nexport { BandwidthEstimator } from './bandwidth-estimator.js';\n\nexport { BufferBasedController } from './buffer-based-controller.js';\nexport type { AbrTrack, AbrSignals, AbrDecision, BufferBasedConfig } from './buffer-based-controller.js';\n","/**\n * Min-heap jitter buffer ordered by (groupId, objectId).\n *\n * Merges objects arriving from multiple subgroups (on separate QUIC streams)\n * into strict decode order. Uses an array-based binary min-heap for\n * O(log N) insert/extract — NOT Array.sort().\n *\n * @see draft-ietf-moq-loc-01 §4.2 (single track: groupId then objectId order)\n * @see draft-ietf-moq-loc-01 §4.3 (temporal layers: cross-subgroup ordering)\n * @module\n */\n\nimport type { MoqtObject, MoqtObjectData } from '@openmoq/transport';\n\n/**\n * Compare two objects by (groupId, objectId) ascending.\n * Returns negative if a < b, positive if a > b, 0 if equal.\n */\nfunction compare(a: MoqtObject, b: MoqtObject): number {\n if (a.groupId < b.groupId) return -1;\n if (a.groupId > b.groupId) return 1;\n if (a.objectId < b.objectId) return -1;\n if (a.objectId > b.objectId) return 1;\n return 0;\n}\n\n/**\n * Min-heap jitter buffer for MoqtObject ordering.\n *\n * @see draft-ietf-moq-loc-01 §4.2, §4.3\n */\nexport class JitterBuffer {\n private readonly heap: MoqtObject[] = [];\n private readonly maxDepth: number;\n\n constructor(maxDepth: number) {\n this.maxDepth = maxDepth;\n }\n\n /** Number of objects currently buffered. */\n get size(): number {\n return this.heap.length;\n }\n\n /**\n * Insert an object into the buffer.\n * @returns true if accepted, false if buffer is full.\n */\n insert(obj: MoqtObject): boolean {\n if (this.heap.length >= this.maxDepth) return false;\n this.heap.push(obj);\n this.bubbleUp(this.heap.length - 1);\n return true;\n }\n\n /**\n * Peek at the minimum object without removing it.\n */\n peek(): MoqtObject | undefined {\n return this.heap[0];\n }\n\n /**\n * Remove and return the minimum object.\n */\n extract(): MoqtObject | undefined {\n if (this.heap.length === 0) return undefined;\n const min = this.heap[0]!;\n const last = this.heap.pop()!;\n if (this.heap.length > 0) {\n this.heap[0] = last;\n this.sinkDown(0);\n }\n return min;\n }\n\n /**\n * Check if the object at (groupId, objectId) is the minimum in the buffer.\n */\n hasContiguous(groupId: bigint, objectId: bigint): boolean {\n if (this.heap.length === 0) return false;\n const top = this.heap[0]!;\n return top.groupId === groupId && top.objectId === objectId;\n }\n\n /**\n * Remove and return all objects for a given group, in sorted order.\n */\n drainGroup(groupId: bigint): MoqtObject[] {\n const result: MoqtObject[] = [];\n const remaining: MoqtObject[] = [];\n\n // Extract all, separate target group from rest\n for (const obj of this.heap) {\n if (obj.groupId === groupId) {\n result.push(obj);\n } else {\n remaining.push(obj);\n }\n }\n\n // Sort the target group's objects by objectId\n result.sort(compare);\n\n // Rebuild heap from remaining\n this.heap.length = 0;\n for (const obj of remaining) {\n this.heap.push(obj);\n }\n this.rebuild();\n\n return result;\n }\n\n /**\n * Remove all objects with groupId less than the given threshold.\n * @returns Number of objects removed.\n */\n discardBefore(groupId: bigint): number {\n const remaining: MoqtObject[] = [];\n let removed = 0;\n\n for (const obj of this.heap) {\n if (obj.groupId < groupId) {\n removed++;\n } else {\n remaining.push(obj);\n }\n }\n\n this.heap.length = 0;\n for (const obj of remaining) {\n this.heap.push(obj);\n }\n this.rebuild();\n\n return removed;\n }\n\n /**\n * Evict the least-important object from the buffer.\n *\n * Finds the data object with the highest publisherPriority number\n * (= lowest importance per §7: \"lower priority number indicates higher\n * priority\"). Tiebreaker: oldest group (lowest groupId).\n *\n * Gap objects and objects with undefined priority are not evictable.\n *\n * @returns The evicted object, or undefined if no candidate exists.\n * @see draft-ietf-moq-transport-16 §7 (Priority-based dropping)\n */\n evictLowestImportance(): MoqtObjectData | undefined {\n if (this.heap.length === 0) return undefined;\n\n let worstIdx = -1;\n let worstPriority = -1;\n let worstGroupId = -1n;\n\n for (let i = 0; i < this.heap.length; i++) {\n const obj = this.heap[i]!;\n if (obj.kind !== 'data' || obj.publisherPriority === undefined) continue;\n\n const p = obj.publisherPriority;\n if (\n p > worstPriority ||\n (p === worstPriority && obj.groupId < worstGroupId)\n ) {\n worstIdx = i;\n worstPriority = p;\n worstGroupId = obj.groupId;\n }\n }\n\n if (worstIdx === -1) return undefined;\n\n const evicted = this.heap[worstIdx]! as MoqtObjectData;\n\n // Remove by swapping with last element and rebuilding\n const last = this.heap.pop()!;\n if (worstIdx < this.heap.length) {\n this.heap[worstIdx] = last;\n this.rebuild();\n }\n\n return evicted;\n }\n\n /**\n * Clear all buffered objects.\n */\n clear(): void {\n this.heap.length = 0;\n }\n\n // ─── Heap internals ─────────────────────────────────────────────\n\n private bubbleUp(index: number): void {\n while (index > 0) {\n const parentIdx = (index - 1) >> 1;\n if (compare(this.heap[index]!, this.heap[parentIdx]!) >= 0) break;\n this.swap(index, parentIdx);\n index = parentIdx;\n }\n }\n\n private sinkDown(index: number): void {\n const length = this.heap.length;\n while (true) {\n let smallest = index;\n const left = 2 * index + 1;\n const right = 2 * index + 2;\n\n if (left < length && compare(this.heap[left]!, this.heap[smallest]!) < 0) {\n smallest = left;\n }\n if (right < length && compare(this.heap[right]!, this.heap[smallest]!) < 0) {\n smallest = right;\n }\n if (smallest === index) break;\n this.swap(index, smallest);\n index = smallest;\n }\n }\n\n private swap(i: number, j: number): void {\n const tmp = this.heap[i]!;\n this.heap[i] = this.heap[j]!;\n this.heap[j] = tmp;\n }\n\n /** Rebuild the heap from an unordered array (O(n)). */\n private rebuild(): void {\n for (let i = (this.heap.length >> 1) - 1; i >= 0; i--) {\n this.sinkDown(i);\n }\n }\n}\n","/**\n * Group-level gap detection and skip-forward logic.\n *\n * Tracks the expected group sequence for a track. Detects when groups\n * are missing (server dropped them under congestion via priority-based\n * dropping). Decides whether to wait or skip forward.\n *\n * @see draft-ietf-moq-transport-16 §10.2.1.1 (Object Status)\n * @see draft-ietf-moq-transport-16 §7 (Priority-based dropping)\n * @module\n */\n\nimport type { ClockSource } from './types.js';\n\n// ─── Action constants ───────────────────────────────────────────────\n\n/**\n * Gap evaluation actions.\n * @see draft-ietf-moq-transport-16 §10.2.1.1\n */\nexport const GapAction = {\n /** Continue waiting for the missing group. */\n WAIT: 'wait',\n /** Skip forward to the next available group with a keyframe. */\n SKIP_FORWARD: 'skip_forward',\n /** Track has ended. */\n TRACK_ENDED: 'track_ended',\n} as const;\n\nexport type GapActionValue = (typeof GapAction)[keyof typeof GapAction];\n\n/**\n * Gap evaluation result.\n */\nexport interface GapDecision {\n readonly action: GapActionValue;\n /** If SKIP_FORWARD, the group to skip to. */\n readonly targetGroupId?: bigint;\n}\n\n// ─── GapDetector ────────────────────────────────────────────────────\n\n/**\n * Detects missing groups and decides when to skip forward.\n *\n * @see draft-ietf-moq-transport-16 §10.2.1.1\n */\nexport class GapDetector {\n private _gapTimeoutUs: number;\n private readonly clock: ClockSource;\n\n /** Set of group IDs explicitly terminated via END_OF_GROUP. */\n private readonly endedGroups = new Set<bigint>();\n\n /** Whether END_OF_TRACK has been received. */\n private trackEnded = false;\n\n /** Earliest observation time for each group. */\n private readonly groupFirstSeenUs = new Map<bigint, number>();\n\n constructor(config: { gapTimeoutUs: number; clock: ClockSource }) {\n this._gapTimeoutUs = config.gapTimeoutUs;\n this.clock = config.clock;\n }\n\n /** Current gap timeout in microseconds. Mutable for adaptive tolerance. */\n get gapTimeoutUs(): number { return this._gapTimeoutUs; }\n set gapTimeoutUs(value: number) { this._gapTimeoutUs = value; }\n\n /**\n * Notify that an object has been received for a group.\n * Records the observation time for timeout calculations.\n */\n observeGroup(groupId: bigint): void {\n if (!this.groupFirstSeenUs.has(groupId)) {\n this.groupFirstSeenUs.set(groupId, this.clock.now());\n }\n }\n\n /**\n * Notify that an END_OF_GROUP status was received.\n * @see draft-ietf-moq-transport-16 §10.2.1.1 — ObjectStatus 0x3\n */\n observeEndOfGroup(groupId: bigint): void {\n this.endedGroups.add(groupId);\n }\n\n /**\n * Notify that an END_OF_TRACK status was received.\n * @see draft-ietf-moq-transport-16 §10.2.1.1 — ObjectStatus 0x4\n */\n observeEndOfTrack(): void {\n this.trackEnded = true;\n }\n\n /** Whether the track has ended. */\n get isTrackEnded(): boolean {\n return this.trackEnded;\n }\n\n /** Reset all state — used when resuming after pause on a live stream. */\n reset(): void {\n this.endedGroups.clear();\n this.trackEnded = false;\n this.groupFirstSeenUs.clear();\n }\n\n /**\n * Evaluate the current gap state.\n *\n * @param lastConsumedGroupId The last group fully consumed by the decoder\n * @param availableGroupIds Groups currently buffered (sorted ascending)\n * @returns Decision: WAIT, SKIP_FORWARD, or TRACK_ENDED\n */\n evaluate(lastConsumedGroupId: bigint, availableGroupIds: bigint[]): GapDecision {\n // Track ended → always report\n if (this.trackEnded) {\n return { action: GapAction.TRACK_ENDED };\n }\n\n // Nothing available → wait\n if (availableGroupIds.length === 0) {\n return { action: GapAction.WAIT };\n }\n\n const nextExpected = lastConsumedGroupId + 1n;\n const sorted = [...availableGroupIds].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));\n const firstAvailable = sorted[0]!;\n\n // Contiguous — next expected is available\n if (firstAvailable <= nextExpected) {\n return { action: GapAction.WAIT };\n }\n\n // Gap exists: firstAvailable > nextExpected\n // Check if all intermediate groups are explicitly ended\n let allEnded = true;\n for (let g = nextExpected; g < firstAvailable; g++) {\n if (!this.endedGroups.has(g)) {\n allEnded = false;\n break;\n }\n }\n\n if (allEnded) {\n // All intermediate groups are explicitly ended → skip immediately\n return { action: GapAction.SKIP_FORWARD, targetGroupId: firstAvailable };\n }\n\n // Implicit gap — check if we've waited long enough since the first\n // available group was observed (that's when the gap became detectable)\n const now = this.clock.now();\n const firstSeenAt = this.groupFirstSeenUs.get(firstAvailable);\n\n if (firstSeenAt !== undefined) {\n const elapsed = now - firstSeenAt;\n if (elapsed >= this.gapTimeoutUs) {\n return { action: GapAction.SKIP_FORWARD, targetGroupId: firstAvailable };\n }\n }\n\n return { action: GapAction.WAIT };\n }\n}\n","/**\n * Decoder lifecycle FSM.\n *\n * Models the decoder's state without touching actual WebCodecs APIs.\n * Tracks whether a keyframe is needed before delta frames can be decoded\n * (after startup or after a gap).\n *\n * Audio is treated as independently decodable (Opus/AAC-LC frames are\n * self-contained) — no keyframe wait needed.\n *\n * ```\n * IDLE ──configure──► NEEDS_KEYFRAME ──keyframe──► DECODING ──gap──► NEEDS_KEYFRAME\n * │\n * END_OF_TRACK\n * │\n * ▼\n * ENDED\n * ```\n *\n * @see draft-ietf-moq-loc-01 §2.3.2.2 (VideoFrameMarking.independent)\n * @module\n */\n\nimport type { VideoChunkInit, AudioChunkInit, VideoFrameMarking } from '@openmoq/loc';\n\n// ─── State constants ────────────────────────────────────────────────\n\n/**\n * Decoder states.\n */\nexport const DecoderState = {\n /** Not yet configured. */\n IDLE: 'idle',\n /** Configured but waiting for a keyframe. */\n NEEDS_KEYFRAME: 'needs_keyframe',\n /** Actively decoding. */\n DECODING: 'decoding',\n /** Track ended. */\n ENDED: 'ended',\n} as const;\n\nexport type DecoderStateValue = (typeof DecoderState)[keyof typeof DecoderState];\n\n// ─── Frame decisions ────────────────────────────────────────────────\n\n/**\n * Decision for each incoming chunk.\n */\nexport type FrameDecision =\n | { readonly action: 'decode'; readonly chunk: VideoChunkInit | AudioChunkInit }\n | { readonly action: 'skip'; readonly reason: string }\n | { readonly action: 'configure'; readonly config: Uint8Array }\n | { readonly action: 'flush' }\n | { readonly action: 'reset'; readonly reason: string };\n\n// ─── DecoderStateMachine ────────────────────────────────────────────\n\n/**\n * Decoder lifecycle FSM.\n *\n * @see draft-ietf-moq-loc-01 §4.2 (decode order)\n */\nexport class DecoderStateMachine {\n private _state: DecoderStateValue = DecoderState.IDLE;\n private readonly _mediaType: 'video' | 'audio';\n private lastConfig: Uint8Array | null = null;\n\n constructor(mediaType: 'video' | 'audio') {\n this._mediaType = mediaType;\n }\n\n get state(): DecoderStateValue {\n return this._state;\n }\n\n get mediaType(): 'video' | 'audio' {\n return this._mediaType;\n }\n\n /**\n * Provide codec configuration.\n *\n * If the config is identical to the last one, skip reconfiguration\n * (stay in current state). This avoids resetting to NEEDS_KEYFRAME\n * on every group boundary when the server sends videoConfig on\n * every group's first object with unchanged codec parameters.\n *\n * Transitions IDLE/DECODING → NEEDS_KEYFRAME (video) or DECODING (audio)\n * only when the config actually changes.\n */\n configure(config: Uint8Array): FrameDecision {\n // Skip if config is unchanged\n if (this.lastConfig !== null && this.configEquals(this.lastConfig, config)) {\n return { action: 'skip', reason: 'config unchanged' };\n }\n\n this.lastConfig = config;\n\n if (this._mediaType === 'audio') {\n // Audio chunks are always independently decodable — go straight to DECODING\n this._state = DecoderState.DECODING;\n } else {\n this._state = DecoderState.NEEDS_KEYFRAME;\n }\n return { action: 'configure', config };\n }\n\n /** Byte-level config comparison. */\n private configEquals(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n }\n\n /**\n * Process a video chunk.\n * @see draft-ietf-moq-loc-01 §2.3.2.2 (VideoFrameMarking)\n */\n processVideoChunk(\n chunk: VideoChunkInit,\n marking: VideoFrameMarking | undefined,\n ): FrameDecision {\n if (this._state === DecoderState.IDLE || this._state === DecoderState.ENDED) {\n return { action: 'skip', reason: `decoder is ${this._state}` };\n }\n\n if (this._state === DecoderState.NEEDS_KEYFRAME) {\n // Need a confirmed independent frame to start decoding\n const isKeyframe = marking?.independent === true;\n if (!isKeyframe) {\n return { action: 'skip', reason: 'waiting for keyframe' };\n }\n this._state = DecoderState.DECODING;\n return { action: 'decode', chunk };\n }\n\n // DECODING — accept any frame\n return { action: 'decode', chunk };\n }\n\n /**\n * Process an audio chunk. Audio codecs (Opus, AAC-LC) produce\n * independently decodable frames — no keyframe gating needed.\n */\n processAudioChunk(chunk: AudioChunkInit): FrameDecision {\n if (this._state === DecoderState.IDLE || this._state === DecoderState.ENDED) {\n return { action: 'skip', reason: `decoder is ${this._state}` };\n }\n\n // Audio is always independently decodable — always decode\n this._state = DecoderState.DECODING;\n return { action: 'decode', chunk };\n }\n\n /**\n * Signal a gap — decoder needs to wait for next keyframe (video only).\n * Audio: stays in DECODING since all chunks are independently decodable.\n */\n notifyGap(): FrameDecision {\n if (this._mediaType === 'audio') {\n // Audio can resume immediately — all chunks are key\n return { action: 'reset', reason: 'gap detected (audio will resume immediately)' };\n }\n this._state = DecoderState.NEEDS_KEYFRAME;\n return { action: 'reset', reason: 'gap detected' };\n }\n\n /**\n * Signal track ended.\n * @see draft-ietf-moq-transport-16 §10.2.1.1\n */\n notifyTrackEnded(): FrameDecision {\n this._state = DecoderState.ENDED;\n return { action: 'flush' };\n }\n}\n","/**\n * A/V sync controller with injectable clock.\n *\n * Audio-master model: the first audio sample establishes a sync reference\n * mapping CaptureTimestamp (wall-clock microseconds) to local clock time.\n * Video render times are computed relative to this reference.\n *\n * Drift detection: reportPresentationTiming measures VIDEO PRESENTATION SCHEDULE\n * drift — actual presentation time minus the exact scheduled time carried with\n * the frame. If it exceeds the adaptive threshold, presentationDriftExceeded\n * signals and a sync_drift event is emitted — but no automatic re-anchor occurs.\n * Actual audio/video skew is a separate metric (sync_skew).\n * The reference is set once to avoid audio glitching from re-anchor jumps.\n * In practice, gap recovery (skip_forward) and seek/pause naturally re-anchor.\n *\n * Live catch-up: measures end-to-end latency by comparing CaptureTimestamp\n * (publisher wall-clock) to subscriber wall-clock. When latency exceeds\n * targetLatency + threshold, activates catch-up (playback rate > 1.0).\n * Hysteresis prevents oscillation near the threshold boundary.\n *\n * @see draft-ietf-moq-loc-01 §2.3.1.1 (CaptureTimestamp)\n * @see draft-ietf-moq-msf-00 §5.1.16 (targetLatency)\n * @module\n */\n\nimport type { ClockSource, CatchUpState } from './types.js';\n\n// ─── Render timing ──────────────────────────────────────────────────\n\n/**\n * Render time calculation result.\n */\nexport interface RenderTiming {\n /** When to render this frame, in local clock microseconds. */\n readonly renderTimeUs: number;\n /** How far ahead (+) or behind (-) this frame is relative to now. */\n readonly offsetUs: number;\n /** Whether this frame should be dropped (too late to render). */\n readonly shouldDrop: boolean;\n}\n\n/** Default: frames more than 500ms late are dropped. */\nconst DEFAULT_DROP_THRESHOLD_US = 500_000;\n\n// ─── Wall clock ─────────────────────────────────────────────────────\n\n/**\n * Injectable wall-clock source for latency measurement.\n * Returns Unix epoch microseconds (same domain as CaptureTimestamp).\n * Default: `Date.now() * 1000`.\n *\n * @see draft-ietf-moq-loc-01 §2.3.1.1 (CaptureTimestamp = wall-clock µs)\n */\nexport interface WallClockSource {\n /** Current wall-clock time in microseconds since Unix epoch. */\n now(): number;\n}\n\n/** Default wall clock — Date.now() converted to microseconds. */\nconst DEFAULT_WALL_CLOCK: WallClockSource = { now: () => Date.now() * 1000 };\n\n// ─── SyncController config ──────────────────────────────────────────\n\nexport interface SyncControllerConfig {\n readonly driftThresholdUs: number;\n readonly dropThresholdUs?: number;\n readonly clock: ClockSource;\n\n // ── Catch-up (§5.1.16 targetLatency) ───────────────────────\n /** Target end-to-end latency in ms. @see draft-ietf-moq-msf-00 §5.1.16 */\n readonly targetLatencyMs?: number;\n /** Max playback rate for catch-up (e.g., 1.1). Default: 1.0 (disabled). */\n readonly maxCatchUpRate?: number;\n /** Latency above target before catch-up activates (ms). Default: 500. */\n readonly catchUpThresholdMs?: number;\n /** Latency above target before catch-up deactivates (ms, hysteresis). Default: 50. */\n readonly catchUpRecoveryMs?: number;\n /** Injectable wall-clock for testability. Default: Date.now() * 1000. */\n readonly wallClock?: WallClockSource;\n}\n\n// ─── SyncController ─────────────────────────────────────────────────\n\n/**\n * A/V sync controller with live catch-up.\n *\n * @see draft-ietf-moq-loc-01 §2.3.1.1\n * @see draft-ietf-moq-msf-00 §5.1.16\n */\nexport class SyncController {\n private _driftThresholdUs: number;\n private readonly dropThresholdUs: number;\n private readonly clock: ClockSource;\n\n /** Local clock time when audio reference was set. */\n private localBaselineUs: number | undefined;\n /** CaptureTimestamp of the audio reference sample. */\n private captureBaselineUs: bigint | undefined;\n\n /** Accumulated drift measurement. */\n private _currentDriftUs = 0;\n\n /**\n * Video join offset — shifts video render times forward during live join.\n *\n * When joining a live stream, video starts from the GOP boundary\n * (which has an older CaptureTimestamp than the current audio).\n * This offset shifts video render times forward so the first frames\n * render in the future instead of the past, preventing startup stutter.\n *\n * Snaps to zero when the video timeline catches up to the audio live edge.\n * Audio render times are unaffected.\n */\n private _videoJoinOffsetUs = 0;\n\n // ── Catch-up state ─────────────────────────────────────────\n private readonly wallClock: WallClockSource;\n private readonly targetLatencyMs: number | undefined;\n private readonly maxCatchUpRate: number;\n private readonly catchUpThresholdMs: number;\n private readonly catchUpRecoveryMs: number;\n\n private _catchUpActive = false;\n private _currentRate = 1.0;\n private _lastLatencyUs = 0;\n\n constructor(config: SyncControllerConfig) {\n this._driftThresholdUs = config.driftThresholdUs;\n this.dropThresholdUs = config.dropThresholdUs ?? DEFAULT_DROP_THRESHOLD_US;\n this.clock = config.clock;\n\n // Catch-up config\n this.wallClock = config.wallClock ?? DEFAULT_WALL_CLOCK;\n this.targetLatencyMs = config.targetLatencyMs;\n this.maxCatchUpRate = config.maxCatchUpRate ?? 1.0;\n this.catchUpThresholdMs = config.catchUpThresholdMs ?? 500;\n this.catchUpRecoveryMs = config.catchUpRecoveryMs ?? 50;\n }\n\n /** Whether a sync reference has been established. */\n get hasReference(): boolean {\n return this.localBaselineUs !== undefined;\n }\n\n /** Current drift magnitude in microseconds. */\n get currentDriftUs(): number {\n return this._currentDriftUs;\n }\n\n /** Current drift threshold in microseconds. Mutable for adaptive tolerance. */\n get driftThresholdUs(): number { return this._driftThresholdUs; }\n set driftThresholdUs(value: number) { this._driftThresholdUs = value; }\n\n /**\n * Whether PRESENTATION SCHEDULE drift exceeds the threshold.\n *\n * Deliberately has NO live-join suppression. The exact queued schedule\n * already contains the join offset, so missing that schedule during a join\n * is a real miss. (The legacy PTS-era metric needed the suppression because\n * the offset itself showed up as drift; see {@link needsResync}.)\n */\n get presentationDriftExceeded(): boolean {\n return Math.abs(this._currentDriftUs) >= this._driftThresholdUs;\n }\n\n /**\n * @deprecated Threshold check for the legacy {@link reportActualRenderTime}\n * metric, retaining its live-join suppression. Use\n * {@link presentationDriftExceeded} with schedule-based measurement.\n */\n get needsResync(): boolean {\n // Suppress during video join — under the LEGACY PTS-anchored metric the\n // offset itself appeared as drift.\n if (this._videoJoinOffsetUs > 0) return false;\n return Math.abs(this._currentDriftUs) >= this._driftThresholdUs;\n }\n\n /** Whether catch-up is currently active. */\n get catchUpActive(): boolean {\n return this._catchUpActive;\n }\n\n /** Current playback rate (1.0 when catch-up inactive). */\n get currentRate(): number {\n return this._currentRate;\n }\n\n /**\n * Last measured latency in microseconds.\n * 0 before first measurement.\n * @see draft-ietf-moq-loc-01 §2.3.1.1\n */\n get latencyUs(): number {\n return this._lastLatencyUs;\n }\n\n /**\n * Establish sync reference from an audio sample.\n * Maps captureTimestamp to the current local clock time.\n * Resets drift measurement.\n *\n * @param captureTimestampUs CaptureTimestamp in microseconds\n */\n setAudioReference(captureTimestampUs: bigint): void {\n this.localBaselineUs = this.clock.now();\n this.captureBaselineUs = captureTimestampUs;\n this._currentDriftUs = 0;\n }\n\n /**\n * Establish sync reference from a video frame (video-only mode).\n * No-op if a reference already exists — audio has priority.\n *\n * @param captureTimestampUs CaptureTimestamp of the first video frame\n */\n setVideoReference(captureTimestampUs: bigint): void {\n if (this.hasReference) return;\n this.localBaselineUs = this.clock.now();\n this.captureBaselineUs = captureTimestampUs;\n this._currentDriftUs = 0;\n }\n\n /** Current video join offset in microseconds. 0 = no offset active. */\n get videoJoinOffsetUs(): number {\n return this._videoJoinOffsetUs;\n }\n\n /**\n * Apply video join re-anchor for live stream join.\n *\n * Called when the first video keyframe is decoded after a live join.\n * Computes an offset that shifts video render times forward so the\n * first frame renders at `now + cushion` instead of in the past.\n *\n * The offset is a constant added to all video render times. It preserves\n * inter-frame timing (33ms spacing) while shifting the whole timeline.\n * Snaps to zero when video catches up to the live edge.\n *\n * @param firstKeyframeCaptureUs CaptureTimestamp of the first keyframe\n * @param cushionUs Extra future cushion (default: 16ms ≈ 1 rAF tick)\n */\n onVideoJoin(firstKeyframeCaptureUs: bigint, cushionUs: number = 16_000): void {\n if (this.localBaselineUs === undefined || this.captureBaselineUs === undefined) return;\n\n const naiveRenderTimeUs = this.localBaselineUs +\n Number(firstKeyframeCaptureUs - this.captureBaselineUs);\n const nowUs = this.clock.now();\n const gapUs = nowUs + cushionUs - naiveRenderTimeUs;\n\n if (gapUs > 0) {\n this._videoJoinOffsetUs = gapUs;\n }\n }\n\n /**\n * Compute render timing for a video frame.\n *\n * @param captureTimestampUs CaptureTimestamp in microseconds\n */\n computeVideoRenderTime(captureTimestampUs: bigint): RenderTiming | null {\n const timing = this.computeRenderTime(captureTimestampUs);\n if (timing === null) return null;\n\n if (this._videoJoinOffsetUs > 0) {\n // Apply offset — shift render time forward\n const adjustedRenderTimeUs = timing.renderTimeUs + this._videoJoinOffsetUs;\n const nowUs = this.clock.now();\n\n // Check if base render time (without offset) is now in the future\n // → video has caught up to live edge, snap offset to zero\n if (timing.renderTimeUs > nowUs) {\n this._videoJoinOffsetUs = 0;\n return timing; // use unmodified timing from here\n }\n\n const offsetUs = adjustedRenderTimeUs - nowUs;\n return {\n renderTimeUs: adjustedRenderTimeUs,\n offsetUs,\n shouldDrop: offsetUs < -this.dropThresholdUs,\n };\n }\n\n return timing;\n }\n\n /**\n * Compute render timing for an audio sample.\n *\n * @param captureTimestampUs CaptureTimestamp in microseconds\n */\n computeAudioRenderTime(captureTimestampUs: bigint): RenderTiming | null {\n return this.computeRenderTime(captureTimestampUs);\n }\n\n /**\n * Record PRESENTATION SCHEDULE drift: did the renderer present the frame\n * when it was told to?\n *\n * `scheduledUs` must be the EXACT timestamp stored with the rendered frame.\n * It already accounts for the playout cushion actually applied to that\n * frame, the live-join offset, decode-output recomputation and late-frame\n * handling — none of which may be recomputed or subtracted later, because an\n * adaptive cushion can move between enqueue and presentation.\n *\n * The schedule is REQUIRED here. Optionality lives at\n * `FrameRenderedFeedback`, where the pipeline suppresses the whole\n * measurement/event transaction cleanly: missing evidence is not drift.\n *\n * Actual audio/video skew is measured separately as `sync_skew`.\n */\n reportPresentationTiming(scheduledUs: number, actualUs: number): void {\n this._currentDriftUs = actualUs - scheduledUs;\n }\n\n /**\n * @deprecated Compares against the UNCUSHIONED PTS-anchored time, so a\n * stream presenting exactly on its intended schedule reports the whole\n * playout cushion as drift. Retained as a compatibility path only; use\n * {@link reportPresentationTiming}.\n */\n reportActualRenderTime(captureTimestampUs: bigint, actualLocalUs: number): void {\n if (this.localBaselineUs === undefined || this.captureBaselineUs === undefined) return;\n\n const expectedRenderUs = this.localBaselineUs +\n Number(captureTimestampUs - this.captureBaselineUs);\n\n this._currentDriftUs = actualLocalUs - expectedRenderUs;\n }\n\n // ─── Live Catch-Up ──────────────────────────────────────────────\n\n /**\n * Measure end-to-end latency from a wall-clock timestamp.\n *\n * Compares the publisher's wall-clock timestamp to the subscriber's\n * wall clock, both in Unix epoch microseconds. A media-time timestamp\n * (LOC-04 with Timescale) has no wall-clock meaning, so latency is\n * unmeasurable and this returns null.\n *\n * @param captureTimestampUs Timestamp in microseconds\n * @param isWallClock False when the timestamp is media time. Undefined means wall clock.\n * @returns Latency in microseconds, or null if unmeasurable\n * @see draft-ietf-moq-loc-01 §2.3.1.1\n * @see draft-ietf-moq-loc-04 §2.3.1.2\n */\n measureLatency(captureTimestampUs: bigint, isWallClock?: boolean): number | null {\n if (captureTimestampUs === 0n || isWallClock === false) return null;\n return this.wallClock.now() - Number(captureTimestampUs);\n }\n\n /**\n * Evaluate catch-up state based on current latency.\n *\n * Hysteresis: activates when latency > target + threshold,\n * deactivates when latency <= target + recovery.\n *\n * Rate is proportional to overshoot, quantized to 0.05x increments.\n *\n * @param captureTimestampUs CaptureTimestamp in microseconds (Unix epoch)\n * @returns CatchUpState if evaluated, null if catch-up is disabled or no timestamp\n * @see draft-ietf-moq-msf-00 §5.1.16 (targetLatency)\n */\n evaluateCatchUp(captureTimestampUs: bigint, isWallClock?: boolean): CatchUpState | null {\n // Measure latency\n const latencyUs = this.measureLatency(captureTimestampUs, isWallClock);\n if (latencyUs === null) return null;\n\n this._lastLatencyUs = latencyUs;\n\n // Catch-up disabled?\n if (this.targetLatencyMs === undefined || this.maxCatchUpRate <= 1.0) return null;\n\n const latencyMs = latencyUs / 1000;\n const targetMs = this.targetLatencyMs;\n\n // Hysteresis: activate/deactivate\n if (!this._catchUpActive && latencyMs > targetMs + this.catchUpThresholdMs) {\n this._catchUpActive = true;\n } else if (this._catchUpActive && latencyMs <= targetMs + this.catchUpRecoveryMs) {\n this._catchUpActive = false;\n this._currentRate = 1.0;\n return { active: false, currentRate: 1.0, latencyMs, targetMs };\n }\n\n if (this._catchUpActive) {\n // Proportional rate: scale linearly with overshoot\n const overshoot = latencyMs - targetMs;\n const ratio = Math.min(Math.max(overshoot / this.catchUpThresholdMs, 0), 1);\n const rawRate = 1.0 + (this.maxCatchUpRate - 1.0) * ratio;\n // Quantize to 0.05x increments\n const quantized = Math.round(rawRate * 20) / 20;\n // Clamp to [1.0, maxCatchUpRate]\n this._currentRate = Math.min(Math.max(quantized, 1.0), this.maxCatchUpRate);\n }\n\n return {\n active: this._catchUpActive,\n currentRate: this._currentRate,\n latencyMs,\n targetMs,\n };\n }\n\n /**\n * Reset the sync reference (e.g., after a skip-forward).\n */\n reset(): void {\n this.localBaselineUs = undefined;\n this.captureBaselineUs = undefined;\n this._currentDriftUs = 0;\n this._videoJoinOffsetUs = 0;\n this._catchUpActive = false;\n this._currentRate = 1.0;\n }\n\n // ─── Internal ───────────────────────────────────────────────────\n\n private computeRenderTime(captureTimestampUs: bigint): RenderTiming | null {\n if (this.localBaselineUs === undefined || this.captureBaselineUs === undefined) {\n return null;\n }\n\n const now = this.clock.now();\n\n const renderTimeUs = this.localBaselineUs +\n Number(captureTimestampUs - this.captureBaselineUs);\n\n const offsetUs = renderTimeUs - now;\n const shouldDrop = offsetUs < -this.dropThresholdUs;\n\n return { renderTimeUs, offsetUs, shouldDrop };\n }\n}\n","/**\n * Recovery controller — decides how to respond to playback failures.\n *\n * Sits between the pipeline and the player. The pipeline detects problems\n * (gaps, buffer overflow, server signals), the recovery controller decides\n * the response (skip, reduce quality, resubscribe, terminate).\n *\n * Purely event-driven: escalation is based on consecutive gap counts,\n * not timing windows. A successful frame decode (notifySuccess) resets\n * the gap counter, closing the feedback loop.\n *\n * Local policy reacting to transport signals:\n * @see draft-ietf-moq-transport-16 §10.2.1.1 (Object Status — gap detection)\n * @see draft-ietf-moq-transport-16 §13.4.3 (TOO_FAR_BEHIND signal)\n * @see draft-ietf-moq-transport-16 §13.4.4 (DELIVERY_TIMEOUT signal)\n * @module\n */\n\n// ─── Trigger types ──────────────────────────────────────────────────\n\n/**\n * What went wrong — input to the recovery controller.\n */\nexport type RecoveryTrigger =\n | { readonly type: 'gap'; readonly groupId: bigint }\n | { readonly type: 'stall'; readonly durationMs: number }\n | { readonly type: 'buffer_overflow' }\n | { readonly type: 'too_far_behind' }\n | { readonly type: 'delivery_timeout' }\n | { readonly type: 'decode_error'; readonly message: string };\n\n// ─── Action types ───────────────────────────────────────────────────\n\n/**\n * What to do about it — output from the recovery controller.\n */\nexport type RecoveryAction =\n | { readonly type: 'skip_forward' }\n | { readonly type: 'reduce_quality'; readonly reason: string }\n | { readonly type: 'pause_subscription'; readonly reason: string }\n | { readonly type: 'resubscribe'; readonly startGroup?: bigint }\n | { readonly type: 'terminate'; readonly reason: string };\n\n// ─── Interface ──────────────────────────────────────────────────────\n\n/**\n * Recovery controller interface.\n *\n * Implementations decide how to respond to playback failures.\n * The player layer consumes the returned actions.\n */\nexport interface RecoveryController {\n evaluate(trigger: RecoveryTrigger): RecoveryAction;\n /** Signal that a frame was successfully decoded — resets gap escalation. */\n notifySuccess?(): void;\n /** Reset all accumulated state (e.g., pause→resume). */\n reset?(): void;\n}\n\n// ─── Default implementation ─────────────────────────────────────────\n\n/** Configuration for the default recovery controller. */\nexport interface DefaultRecoveryConfig {\n /** Number of consecutive gaps that triggers quality reduction. Default: 3. */\n readonly gapEscalationThreshold?: number;\n /** Maximum decode errors before terminating. Default: 2. */\n readonly maxDecodeErrors?: number;\n /**\n * Window-based escalation: emit `reduce_quality` when this many\n * gap events fire within `windowedEscalationWindowMs`, regardless\n * of interleaved `notifySuccess()` calls. Catches the pattern\n * where gaps alternate with successful decodes (so the consecutive\n * counter never reaches threshold) but the overall stall rate is\n * unacceptable. Default: 5.\n */\n readonly windowedEscalationThreshold?: number;\n /**\n * Time window (ms) for the windowed escalation counter.\n * Gap timestamps older than this are evicted before checking the\n * threshold. Default: 30_000 (30 seconds).\n */\n readonly windowedEscalationWindowMs?: number;\n /**\n * Minimum stall duration (ms) to count as a quality-degradation\n * signal. Short stalls (50-100ms) are normal jitter; sustained\n * stalls (300ms+) indicate the network can't keep up. Default: 300.\n */\n readonly stallThresholdMs?: number;\n}\n\n/**\n * Default recovery controller — event-driven escalation.\n *\n * Policy:\n * - Single gap → skip_forward\n * - N consecutive gaps → reduce_quality (N = gapEscalationThreshold)\n * - M gaps within W seconds → reduce_quality (windowed escalation,\n * catches alternating gap/success patterns that reset the\n * consecutive counter)\n * - After reduce_quality, further gaps → skip_forward until notifySuccess()\n * - notifySuccess() (frame decoded) resets gap counter + re-arms escalation\n * - buffer_overflow → reduce_quality\n * - too_far_behind → reduce_quality (reacting to §13.4.3 signal)\n * - delivery_timeout → resubscribe (reacting to §13.4.4 signal)\n * - decode_error → resubscribe (first), terminate (repeated)\n */\nexport class DefaultRecoveryController implements RecoveryController {\n private readonly gapEscalationThreshold: number;\n private readonly maxDecodeErrors: number;\n private readonly windowedThreshold: number;\n private readonly windowedWindowUs: number;\n private readonly stallThresholdMs: number;\n private readonly clock: { now(): number } | null;\n\n /** Consecutive gap events without a successful decode. */\n private consecutiveGaps = 0;\n\n /**\n * True after reduce_quality fires — gates further escalation until\n * notifySuccess() confirms the quality reduction helped.\n */\n private qualityReductionPending = false;\n\n /** Number of decode errors seen (for escalation). */\n private decodeErrorCount = 0;\n\n /** Timestamps (µs) of recent gap events for windowed escalation. */\n private readonly gapTimestamps: number[] = [];\n\n constructor(config?: DefaultRecoveryConfig) {\n this.gapEscalationThreshold = config?.gapEscalationThreshold ?? 3;\n this.maxDecodeErrors = config?.maxDecodeErrors ?? 2;\n this.windowedThreshold = config?.windowedEscalationThreshold ?? 5;\n this.windowedWindowUs = (config?.windowedEscalationWindowMs ?? 30_000) * 1000;\n this.stallThresholdMs = config?.stallThresholdMs ?? 300;\n this.clock = null;\n }\n\n /**\n * Attach a clock source for windowed escalation. Without a clock\n * the windowed path is silently disabled (consecutive-only policy).\n * Kept as a setter rather than a constructor param so existing\n * callers that don't need windowed escalation don't break.\n */\n setClock(clock: { now(): number }): void {\n (this as unknown as { clock: { now(): number } | null }).clock = clock;\n }\n\n evaluate(trigger: RecoveryTrigger): RecoveryAction {\n switch (trigger.type) {\n case 'gap':\n return this.handleGap();\n\n case 'stall':\n return this.handleStall(trigger.durationMs);\n\n case 'buffer_overflow':\n return this.escalateOrSkip('jitter buffer overflow');\n\n case 'too_far_behind':\n return this.escalateOrSkip('server reported TOO_FAR_BEHIND');\n\n case 'delivery_timeout':\n return { type: 'resubscribe' };\n\n case 'decode_error':\n return this.handleDecodeError(trigger.message);\n }\n }\n\n /**\n * Signal that a frame was successfully decoded/rendered.\n *\n * Resets the consecutive gap counter and re-arms quality escalation.\n * This closes the feedback loop: gaps → reduce_quality → better frames\n * arrive → notifySuccess → ready for next escalation if needed.\n */\n notifySuccess(): void {\n this.consecutiveGaps = 0;\n this.qualityReductionPending = false;\n }\n\n /** Reset all state — e.g., after pause→resume. */\n reset(): void {\n this.consecutiveGaps = 0;\n this.qualityReductionPending = false;\n this.decodeErrorCount = 0;\n this.gapTimestamps.length = 0;\n }\n\n // ─── Internal ───────────────────────────────────────────────────\n\n /**\n * Request quality reduction, gated by the pending flag.\n * If a reduce_quality is already pending (waiting for notifySuccess),\n * return skip_forward instead to prevent spam.\n */\n private escalateOrSkip(reason: string): RecoveryAction {\n if (this.qualityReductionPending) {\n return { type: 'skip_forward' };\n }\n this.qualityReductionPending = true;\n return { type: 'reduce_quality', reason };\n }\n\n private handleGap(): RecoveryAction {\n this.consecutiveGaps++;\n\n // Consecutive-gap escalation (original policy)\n if (this.consecutiveGaps >= this.gapEscalationThreshold) {\n this.consecutiveGaps = 0;\n this.gapTimestamps.length = 0;\n return this.escalateOrSkip('repeated gaps detected');\n }\n\n // Windowed escalation: catches the alternating gap/success\n // pattern where notifySuccess() resets the consecutive counter\n // between every gap but the overall stall rate is unacceptable.\n if (this.clock) {\n const now = this.clock.now();\n this.gapTimestamps.push(now);\n // Evict timestamps outside the window\n const cutoff = now - this.windowedWindowUs;\n while (this.gapTimestamps.length > 0 && this.gapTimestamps[0]! < cutoff) {\n this.gapTimestamps.shift();\n }\n if (this.gapTimestamps.length >= this.windowedThreshold) {\n this.gapTimestamps.length = 0;\n this.consecutiveGaps = 0;\n return this.escalateOrSkip('repeated stalls within window');\n }\n }\n\n return { type: 'skip_forward' };\n }\n\n /**\n * Relay signals overload via PUBLISH_DONE/TOO_FAR_BEHIND (§9.13);\n * network bottlenecks produce no server signal, so the player must\n * self-detect via stall rate.\n */\n private handleStall(durationMs: number): RecoveryAction {\n if (durationMs < this.stallThresholdMs) {\n return { type: 'skip_forward' };\n }\n // Feed into the same windowed counter as gaps — the user doesn't\n // care WHY the video is degraded, only that it is.\n if (this.clock) {\n const now = this.clock.now();\n this.gapTimestamps.push(now);\n const cutoff = now - this.windowedWindowUs;\n while (this.gapTimestamps.length > 0 && this.gapTimestamps[0]! < cutoff) {\n this.gapTimestamps.shift();\n }\n if (this.gapTimestamps.length >= this.windowedThreshold) {\n this.gapTimestamps.length = 0;\n return this.escalateOrSkip('sustained stalls within window');\n }\n }\n return { type: 'skip_forward' };\n }\n\n private handleDecodeError(message: string): RecoveryAction {\n this.decodeErrorCount++;\n if (this.decodeErrorCount >= this.maxDecodeErrors) {\n return { type: 'terminate', reason: `repeated decode error: ${message}` };\n }\n return { type: 'resubscribe' };\n }\n}\n","/**\n * Adaptive jitter tolerance controller.\n *\n * Auto-calibrates gap timeout and drift threshold from observed network\n * jitter. Implements graduated recovery (monitoring → extended wait → skip)\n * with stall-count escalation and exponential decay.\n *\n * Sans-I/O: time is an explicit parameter, no browser APIs.\n * All state is flat and mutable in place — zero allocations in the hot path.\n *\n * @see draft-ietf-moq-loc-01 §2.3.1.1 (CaptureTimestamp)\n * @module\n */\n\n// ─── Recovery phases ────────────────────────────────────────────────\n\nexport const RecoveryPhase = {\n /** Baseline — no active jitter issues. */\n Normal: 'normal',\n /** Gap detected, observing whether transient or persistent. */\n Monitoring: 'monitoring',\n /** Persistent gap — waiting with extended timeout before skipping. */\n ExtendedWait: 'extended_wait',\n} as const;\n\nexport type RecoveryPhaseValue = (typeof RecoveryPhase)[keyof typeof RecoveryPhase];\n\n// ─── Configuration ──────────────────────────────────────────────────\n\nexport interface ToleranceConfig {\n // Jitter estimation\n readonly emaAlpha: number;\n readonly varianceAlpha: number;\n readonly slidingWindowSize: number;\n readonly jitterMultiplier: number;\n\n // Gap timeout bounds\n readonly minGapTimeoutMs: number;\n readonly maxGapTimeoutMs: number;\n\n // Drift auto-calibration\n readonly minDriftThresholdMs: number;\n readonly driftThresholdMultiplier: number;\n\n // Stall escalation\n readonly stallIncrementMs: number;\n readonly maxStallIncrements: number;\n\n // Recovery phases\n readonly monitorWindowMs: number;\n readonly monitorCleanFrames: number;\n readonly monitorCleanDurationMs: number;\n readonly maxExtendedWaitMs: number;\n\n // Skip frequency → quality reduction\n readonly skipFrequencyThreshold: number;\n readonly skipFrequencyWindowMs: number;\n\n // Decay\n readonly decayMinCleanFrames: number;\n readonly decayMinCleanDurationMs: number;\n readonly decayFactor: number;\n}\n\nexport const DEFAULT_TOLERANCE_CONFIG: ToleranceConfig = {\n emaAlpha: 0.125, // 1/8 — RFC 6298 gain\n varianceAlpha: 0.25, // 1/4 — RFC 6298 gain\n slidingWindowSize: 128, // ~4s at 30fps\n jitterMultiplier: 4.0, // μ + 4σ ≈ 99.99% of Gaussian\n\n minGapTimeoutMs: 50, // floor: ~1.5 video frames\n maxGapTimeoutMs: 2000, // ceiling\n\n minDriftThresholdMs: 80,\n driftThresholdMultiplier: 3.0,\n\n stallIncrementMs: 33, // 1 video frame interval\n maxStallIncrements: 3, // max 3 × 33ms = ~100ms added\n\n monitorWindowMs: 500, // observe before escalating\n monitorCleanFrames: 60, // 1s at 60fps tick rate\n monitorCleanDurationMs: 1000,\n maxExtendedWaitMs: 2000,\n\n skipFrequencyThreshold: 3, // 3 skips in window → quality reduce\n skipFrequencyWindowMs: 30000,\n\n decayMinCleanFrames: 120, // 2s at 60fps\n decayMinCleanDurationMs: 2000,\n decayFactor: 0.9, // 10% reduction per decay step\n};\n\n// ─── Tick result ────────────────────────────────────────────────────\n\nexport interface TickResult {\n /** Whether the caller should skip forward past the current gap. */\n readonly shouldSkip: boolean;\n}\n\nconst TICK_NO_SKIP: TickResult = { shouldSkip: false };\nconst TICK_SKIP: TickResult = { shouldSkip: true };\n\n// ─── Sliding window max ─────────────────────────────────────────────\n\n/**\n * O(1) amortized sliding window maximum using a monotonic deque.\n * Pre-allocated, zero-alloc after construction.\n */\nclass SlidingMax {\n private readonly buf: Float64Array;\n private readonly deq: Uint32Array;\n private readonly capacity: number;\n private writePos = 0;\n private count = 0;\n private dHead = 0;\n private dTail = 0;\n\n constructor(capacity: number) {\n this.capacity = capacity;\n this.buf = new Float64Array(capacity);\n this.deq = new Uint32Array(capacity);\n }\n\n push(value: number): void {\n const cap = this.capacity;\n const wIdx = this.writePos % cap;\n\n // Evict expired front\n if (this.count === cap && this.dHead !== this.dTail\n && this.deq[this.dHead % cap] === wIdx) {\n this.dHead++;\n }\n\n // Maintain monotonicity from back\n while (this.dHead !== this.dTail\n && this.buf[this.deq[(this.dTail - 1) % cap]! % cap]! <= value) {\n this.dTail--;\n }\n\n this.deq[this.dTail % cap] = wIdx;\n this.dTail++;\n this.buf[wIdx] = value;\n this.writePos++;\n if (this.count < cap) this.count++;\n }\n\n get max(): number {\n if (this.dHead === this.dTail) return 0;\n return this.buf[this.deq[this.dHead % this.capacity]! % this.capacity]!;\n }\n\n reset(): void {\n this.writePos = 0;\n this.count = 0;\n this.dHead = 0;\n this.dTail = 0;\n }\n}\n\n// ─── Controller ─────────────────────────────────────────────────────\n\n/**\n * Adaptive jitter tolerance controller.\n *\n * Consumes frame arrival and gap events, produces auto-calibrated\n * thresholds for gap timeout and drift detection.\n */\nexport class AdaptiveToleranceController {\n private readonly cfg: ToleranceConfig;\n\n // ── Jitter estimation ──\n private _jitterEma = 0;\n private _jitterVar = 0;\n private _lastTransitMs = 0;\n private _hasLastTransit = false;\n private readonly _slidingMax: SlidingMax;\n\n // ── Phase state ──\n private _phase: RecoveryPhaseValue = RecoveryPhase.Normal;\n private _phaseEnteredMs = 0;\n private _monitorGapCount = 0;\n private _stallCount = 0;\n private _stallEscalationMs = 0;\n\n // ── Skip frequency tracking ──\n private _skipTimestamps: number[] = [];\n\n // ── Clean streak tracking (for monitoring exit + decay) ──\n private _cleanFrameCount = 0;\n private _cleanStreakStartMs = 0;\n\n // ── Quality reduction ──\n private _shouldReduceQuality = false;\n\n // ── Protocol signals ──\n private _deliveryTimeoutMs = 0;\n\n // ── Capture timestamp jitter (for drift threshold) ──\n private _lastCaptureUs = 0;\n private _hasCaptureRef = false;\n private _captureJitterEma = 0;\n\n constructor(config: ToleranceConfig) {\n this.cfg = config;\n this._slidingMax = new SlidingMax(config.slidingWindowSize);\n }\n\n // ─── Read-only state ──────────────────────────────────────────\n\n get phase(): RecoveryPhaseValue { return this._phase; }\n get jitterEmaMs(): number { return this._jitterEma; }\n get jitterVarMs(): number { return this._jitterVar; }\n get slidingMaxMs(): number { return this._slidingMax.max; }\n get stallCount(): number { return this._stallCount; }\n get shouldReduceQuality(): boolean { return this._shouldReduceQuality; }\n\n /**\n * Auto-calibrated gap timeout in milliseconds.\n *\n * baseline = max(minGap, EMA + K × variance, slidingMax)\n * effective = min(baseline + stallEscalation, maxGap, deliveryTimeout)\n */\n get effectiveGapTimeoutMs(): number {\n const adaptive = Math.max(\n this.cfg.minGapTimeoutMs,\n this._jitterEma + this.cfg.jitterMultiplier * this._jitterVar,\n this._slidingMax.max,\n );\n const withStall = adaptive + this._stallEscalationMs;\n let capped = Math.min(withStall, this.cfg.maxGapTimeoutMs);\n\n if (this._deliveryTimeoutMs > 0) {\n capped = Math.min(capped, this._deliveryTimeoutMs);\n }\n\n return capped;\n }\n\n /**\n * Auto-calibrated drift threshold in milliseconds.\n *\n * Based on observed CaptureTimestamp interarrival jitter.\n */\n get effectiveDriftThresholdMs(): number {\n if (!this._hasCaptureRef || this._captureJitterEma === 0) {\n return this.cfg.minDriftThresholdMs;\n }\n return Math.max(\n this.cfg.minDriftThresholdMs,\n this._captureJitterEma * this.cfg.driftThresholdMultiplier,\n );\n }\n\n // ─── Events IN ────────────────────────────────────────────────\n\n /**\n * Notify that a frame arrived.\n *\n * @param nowMs Current time in milliseconds\n * @param captureUs CaptureTimestamp in microseconds\n * @param arrivalMs Arrival time in milliseconds\n */\n onFrameArrived(nowMs: number, captureUs: number, arrivalMs: number): void {\n // Update interarrival jitter EMA (EWMA over transit deltas)\n const transitMs = arrivalMs - captureUs * 0.001;\n\n if (this._hasLastTransit) {\n const d = transitMs - this._lastTransitMs;\n const absD = d < 0 ? -d : d;\n\n // EWMA update: α = 1/8, β = 1/4 (RFC 6298)\n const emaErr = absD - this._jitterEma;\n this._jitterEma += this.cfg.emaAlpha * emaErr;\n this._jitterVar += this.cfg.varianceAlpha * (absD - this._jitterVar);\n\n // Sliding window max\n this._slidingMax.push(absD);\n }\n\n this._lastTransitMs = transitMs;\n this._hasLastTransit = true;\n\n // Capture timestamp jitter (for drift threshold)\n if (this._hasCaptureRef) {\n const captureIntervalUs = captureUs - this._lastCaptureUs;\n // Expected ~33333 for 30fps, ~20000 for audio — we don't know, just track variance\n const captureJitterMs = Math.abs(captureIntervalUs) * 0.001;\n // Track the variance of capture intervals, not absolute value\n if (this._captureJitterEma === 0) {\n this._captureJitterEma = captureJitterMs;\n } else {\n this._captureJitterEma += this.cfg.emaAlpha * (captureJitterMs - this._captureJitterEma);\n }\n }\n this._lastCaptureUs = captureUs;\n this._hasCaptureRef = true;\n\n // Clean streak tracking\n this._cleanFrameCount++;\n if (this._cleanFrameCount === 1) {\n this._cleanStreakStartMs = nowMs;\n }\n }\n\n /**\n * Notify that a gap was detected.\n *\n * @param nowMs Current time in milliseconds\n * @param groupId The group ID where the gap was detected\n */\n onGapDetected(nowMs: number, _groupId: number): void {\n this._cleanFrameCount = 0;\n\n switch (this._phase) {\n case RecoveryPhase.Normal:\n this._phase = RecoveryPhase.Monitoring;\n this._phaseEnteredMs = nowMs;\n this._monitorGapCount = 1;\n break;\n\n case RecoveryPhase.Monitoring:\n this._monitorGapCount++;\n if (this._monitorGapCount >= 2\n && nowMs - this._phaseEnteredMs <= this.cfg.monitorWindowMs) {\n // Persistent gap — escalate\n this._phase = RecoveryPhase.ExtendedWait;\n this._phaseEnteredMs = nowMs;\n }\n break;\n\n case RecoveryPhase.ExtendedWait:\n // Already waiting — no further escalation\n break;\n }\n }\n\n /**\n * Periodic tick — evaluate phase transitions and decay.\n *\n * @param nowMs Current time in milliseconds\n * @returns TickResult indicating whether caller should skip forward\n */\n tick(nowMs: number): TickResult {\n switch (this._phase) {\n case RecoveryPhase.Monitoring: {\n // Check if monitoring window expired without 2nd gap → clean exit\n if (nowMs - this._phaseEnteredMs > this.cfg.monitorWindowMs) {\n // Check clean period\n if (this._cleanFrameCount >= this.cfg.monitorCleanFrames\n && nowMs - this._cleanStreakStartMs >= this.cfg.monitorCleanDurationMs) {\n this._phase = RecoveryPhase.Normal;\n return TICK_NO_SKIP;\n }\n }\n break;\n }\n\n case RecoveryPhase.ExtendedWait: {\n if (nowMs - this._phaseEnteredMs >= this.cfg.maxExtendedWaitMs) {\n // Timeout — skip forward\n this._stallCount++;\n this._stallEscalationMs = Math.min(\n this._stallCount * this.cfg.stallIncrementMs,\n this.cfg.maxStallIncrements * this.cfg.stallIncrementMs,\n );\n\n // Track skip frequency for quality reduction\n this._skipTimestamps.push(nowMs);\n this._pruneSkipTimestamps(nowMs);\n if (this._skipTimestamps.length >= this.cfg.skipFrequencyThreshold) {\n this._shouldReduceQuality = true;\n }\n\n this._phase = RecoveryPhase.Normal;\n this._cleanFrameCount = 0;\n return TICK_SKIP;\n }\n break;\n }\n\n case RecoveryPhase.Normal: {\n // Decay stall escalation during clean periods\n if (this._stallEscalationMs > 0\n && this._cleanFrameCount >= this.cfg.decayMinCleanFrames\n && this._cleanStreakStartMs > 0\n && nowMs - this._cleanStreakStartMs >= this.cfg.decayMinCleanDurationMs) {\n this._stallEscalationMs *= this.cfg.decayFactor;\n if (this._stallEscalationMs < 1) {\n this._stallEscalationMs = 0;\n this._stallCount = 0;\n }\n // Reset clean counters for next decay step\n this._cleanFrameCount = 0;\n this._cleanStreakStartMs = nowMs;\n }\n\n // Decay quality reduction flag\n if (this._shouldReduceQuality) {\n this._pruneSkipTimestamps(nowMs);\n if (this._skipTimestamps.length < this.cfg.skipFrequencyThreshold) {\n this._shouldReduceQuality = false;\n }\n }\n break;\n }\n }\n\n return TICK_NO_SKIP;\n }\n\n /**\n * Set DELIVERY_TIMEOUT from protocol negotiation.\n * Caps the effective gap timeout.\n *\n * @see draft-ietf-moq-transport-16 §9.2.2.2\n */\n setDeliveryTimeout(timeoutMs: number): void {\n this._deliveryTimeoutMs = timeoutMs;\n }\n\n /** Reset all state (seek, pause→resume). */\n reset(): void {\n this._jitterEma = 0;\n this._jitterVar = 0;\n this._lastTransitMs = 0;\n this._hasLastTransit = false;\n this._slidingMax.reset();\n\n this._phase = RecoveryPhase.Normal;\n this._phaseEnteredMs = 0;\n this._monitorGapCount = 0;\n this._stallCount = 0;\n this._stallEscalationMs = 0;\n\n this._skipTimestamps = [];\n this._cleanFrameCount = 0;\n this._cleanStreakStartMs = 0;\n this._shouldReduceQuality = false;\n this._deliveryTimeoutMs = 0;\n\n this._lastCaptureUs = 0;\n this._hasCaptureRef = false;\n this._captureJitterEma = 0;\n }\n\n // ─── Internal ─────────────────────────────────────────────────\n\n private _pruneSkipTimestamps(nowMs: number): void {\n const windowStart = nowMs - this.cfg.skipFrequencyWindowMs;\n this._skipTimestamps = this._skipTimestamps.filter(t => t >= windowStart);\n }\n}\n","/**\n * PTS-anchored render scheduler.\n *\n * Computes wall-clock render times from CaptureTimestamps at decode OUTPUT,\n * solving the async decode latency problem. Anchors on the first rendered\n * frame's PTS and spaces subsequent frames by PTS delta.\n *\n * This produces correct 33ms pacing regardless of delivery pattern:\n * - Burst delivery (cached GOP): 8 frames decoded in 40ms → render 33ms apart\n * - Live delivery: frames arrive at 33ms intervals → render at 33ms intervals\n * - Burst-to-live transition: automatic rebase when delivery catches up to PTS\n *\n * Used by the CanvasRenderer to schedule frame presentation.\n * Sans-I/O: depends only on ClockSource, no browser APIs.\n *\n * @see draft-ietf-moq-loc-01 §2.3.1.1 (CaptureTimestamp)\n * @module\n */\n\nimport type { ClockSource } from './types.js';\n\nexport interface RenderSchedulerOptions {\n /** Extra future cushion for first frame in microseconds. Default: 16000 (1 rAF tick). */\n readonly cushionUs?: number;\n}\n\n/**\n * Computes wall-clock render times from CaptureTimestamps.\n *\n * Call `schedule(captureTimestampUs)` when a decoded frame arrives.\n * Returns the wall-clock time (in µs) when the frame should be painted.\n */\nexport class RenderScheduler {\n private readonly clock: ClockSource;\n private readonly cushionUs: number;\n\n /** Wall-clock anchor: local time when the first frame was scheduled. */\n private baseTimeUs: number | null = null;\n /** PTS anchor: captureTimestamp of the first frame. */\n private basePtsUs: number | null = null;\n\n /** Previous frame's arrival time — for burst-to-live detection. */\n private lastArrivalUs: number = 0;\n\n constructor(clock: ClockSource, options?: RenderSchedulerOptions) {\n this.clock = clock;\n this.cushionUs = options?.cushionUs ?? 16_000;\n }\n\n /**\n * Compute the wall-clock render time for a decoded frame.\n *\n * @param captureTimestampUs CaptureTimestamp from LOC header (microseconds)\n * @returns Wall-clock time in microseconds when the frame should be painted\n */\n schedule(captureTimestampUs: bigint): number {\n const nowUs = this.clock.now();\n const ptsUs = Number(captureTimestampUs);\n\n // INVARIANT: baseTimeUs and basePtsUs are always co-null or co-set —\n // every write site (init / first-frame / rebase / reset) updates both.\n // The `||` here is required for TypeScript narrowing: with `&&`, TS\n // can't prove either is non-null in the else branch.\n if (this.baseTimeUs === null || this.basePtsUs === null) {\n // First frame: set anchor\n this.baseTimeUs = nowUs + this.cushionUs;\n this.basePtsUs = ptsUs;\n this.lastArrivalUs = nowUs;\n return this.baseTimeUs;\n }\n\n // PTS-relative render time\n const ptsDelta = ptsUs - this.basePtsUs;\n let renderTimeUs = this.baseTimeUs + ptsDelta;\n\n // Burst-to-live transition detection:\n // During burst, frames arrive faster than their PTS spacing (inter-arrival < 33ms).\n // During live, frames arrive at PTS spacing (inter-arrival ≈ 33ms).\n // When the scheduled render time falls behind the current clock (render time is in\n // the past), and the frame is arriving at real-time rate, snap the anchor forward.\n const arrivalDelta = nowUs - this.lastArrivalUs;\n\n if (renderTimeUs < nowUs && arrivalDelta > 20_000) {\n // Frame is late AND arrived at real-time rate (not burst)\n // → transition from burst to live, rebase anchor\n this.baseTimeUs = nowUs + this.cushionUs;\n this.basePtsUs = ptsUs;\n renderTimeUs = this.baseTimeUs;\n }\n\n this.lastArrivalUs = nowUs;\n return renderTimeUs;\n }\n\n /** Reset anchor (seek, pause→resume, gap skip). */\n reset(): void {\n this.baseTimeUs = null;\n this.basePtsUs = null;\n this.lastArrivalUs = 0;\n }\n}\n","/**\n * StartupBuffer — holds frames before first drain to warm the jitter estimator.\n *\n * Prevents initial stutter by collecting N frames (determined by network class)\n * before allowing the pipeline to start draining. During the hold, the adaptive\n * tolerance controller calibrates from real arrival timing — so it's ready\n * when playback begins.\n *\n * Network classification uses the QUIC handshake RTT (available before any media)\n * to set initial parameters:\n * - LAN (<1ms): 2 frames / 80ms max — near-zero startup delay\n * - Near-LAN (1-5ms): 3 frames / 120ms\n * - Metro WAN (5-50ms): 8 frames / 300ms\n * - Long-haul WAN (>50ms): 12 frames / 500ms\n *\n * @see draft-ietf-moq-loc-01 §4.2 (decode order)\n * @module\n */\n\n// ─── Network classification ─────────────────────────────────────────\n\nexport interface StartupParams {\n /** Minimum frames to collect before allowing drain. */\n readonly minFrames: number;\n /** Hard timeout — drain regardless of frame count. */\n readonly maxWaitMs: number;\n}\n\n/**\n * Classify network conditions from QUIC handshake RTT.\n *\n * @param handshakeRttMs Round-trip time from the QUIC/WebTransport handshake\n */\nexport function classifyNetwork(handshakeRttMs: number): StartupParams {\n if (handshakeRttMs < 1) {\n return { minFrames: 2, maxWaitMs: 80 };\n } else if (handshakeRttMs < 5) {\n return { minFrames: 3, maxWaitMs: 120 };\n } else if (handshakeRttMs < 50) {\n return { minFrames: 8, maxWaitMs: 300 };\n } else {\n return { minFrames: 12, maxWaitMs: 500 };\n }\n}\n\n// ─── StartupBuffer ──────────────────────────────────────────────────\n\n/**\n * Gates pipeline draining until enough frames have arrived for smooth startup.\n *\n * Usage:\n * ```ts\n * // In pipeline.pushObject():\n * startupBuffer.recordFrame(clock.now() / 1000);\n *\n * // In pipeline.tick():\n * if (!startupBuffer.shouldDrain) return; // hold frames\n * // ... normal drain logic\n * ```\n */\nexport class StartupBuffer {\n private readonly minFrames: number;\n private readonly maxWaitMs: number;\n\n private _frameCount = 0;\n private _firstFrameMs = 0;\n private _lastFrameMs = 0;\n private _draining = false;\n\n constructor(params: StartupParams) {\n this.minFrames = params.minFrames;\n this.maxWaitMs = params.maxWaitMs;\n }\n\n /**\n * Record a frame arrival. Call from pushObject().\n * @param nowMs Current time in milliseconds\n */\n recordFrame(nowMs: number): void {\n this._frameCount++;\n if (this._frameCount === 1) {\n this._firstFrameMs = nowMs;\n }\n this._lastFrameMs = nowMs;\n\n // Check transition\n if (!this._draining) {\n const elapsed = this._lastFrameMs - this._firstFrameMs;\n if (this._frameCount >= this.minFrames || elapsed >= this.maxWaitMs) {\n this._draining = true;\n }\n }\n }\n\n /** Whether the pipeline should start draining (buffer phase complete). */\n get shouldDrain(): boolean {\n return this._draining;\n }\n\n /** Number of frames recorded. */\n get frameCount(): number {\n return this._frameCount;\n }\n\n /** Time elapsed since first frame in milliseconds. */\n get elapsedMs(): number {\n if (this._frameCount === 0) return 0;\n return this._lastFrameMs - this._firstFrameMs;\n }\n\n /** Accelerated EMA alpha for warmup (4x standard 1/16). */\n get warmupEmaAlpha(): number {\n return 0.25;\n }\n\n /** Reset to buffering state (e.g., after seek or pause→resume). */\n reset(): void {\n this._frameCount = 0;\n this._firstFrameMs = 0;\n this._lastFrameMs = 0;\n this._draining = false;\n }\n}\n","/**\n * Keyframe payload validation — verifies codec bitstream actually starts\n * with an independently decodable frame.\n *\n * Used to detect broken publishers that start MoQ groups with delta frames\n * instead of keyframes. Per LOC §4.2, the first object in a group MUST be\n * independently decodable (keyframe/IDR/key_frame).\n *\n * Supports H.264 (AVC), H.265 (HEVC), and AV1.\n * Returns null for unknown codecs (no opinion).\n *\n * @see draft-ietf-moq-loc-01 §4.2 (decode order)\n * @see ITU-T H.264 §7.3.1 (NAL unit syntax)\n * @see ITU-T H.265 §7.3.1.2 (NAL unit header)\n * @see AV1 §5.3.1 (OBU header)\n * @module\n */\n\n/**\n * Check if a video payload starts with a keyframe.\n *\n * @param codec Codec string from catalog (e.g., 'avc1.42c01f', 'hvc1', 'av01.0.08M.10')\n * @param payload Raw bitstream payload from MoQ object\n * @returns true = keyframe, false = delta, null = can't determine (unknown codec or too short)\n */\nexport function isKeyframePayload(codec: string, payload: Uint8Array): boolean | null {\n const codecBase = codec.split('.')[0]!.toLowerCase();\n\n switch (codecBase) {\n case 'avc1':\n case 'avc3':\n return isH264Keyframe(payload);\n case 'hvc1':\n case 'hev1':\n return isH265Keyframe(payload);\n case 'av01':\n return isAV1Keyframe(payload);\n default:\n return null; // Unknown codec — no opinion\n }\n}\n\n/**\n * H.264: check for IDR slice (NAL type 5) or SPS (NAL type 7).\n *\n * LOC payloads use length-prefixed NAL units (AVCC format):\n * [4-byte length][NAL unit][4-byte length][NAL unit]...\n *\n * NAL header byte: forbidden_zero_bit(1) + nal_ref_idc(2) + nal_unit_type(5)\n * Keyframe NAL types: 5 (IDR slice), 7 (SPS), 8 (PPS)\n * Non-IDR types to skip: 6 (SEI), 9 (AUD)\n *\n * @see ITU-T H.264 §7.4.1 (NAL unit semantics)\n */\n/**\n * Hard upper bound on NAL units inspected per access unit.\n *\n * Most real keyframe AUs have well under 32 NAL units (AUD + SPS + PPS +\n * SEI + a few slices). Multi-slice encoding (parallelism, low-latency,\n * error resilience) and SEI-heavy streams (HDR, captions, timecodes) can\n * push the count higher; HEVC tiles and SVC layers can push it higher\n * still. 256 is generous enough for legitimate content while preventing\n * pathological loops on crafted payloads (e.g., zero-length-NAL spam\n * or boundary bugs that fail to advance `pos`).\n *\n * If you hit this bound on a real stream, log + raise — the bound is\n * defense-in-depth, not a spec claim.\n */\nconst MAX_NAL_UNITS_PER_AU = 256;\n\nfunction isH264Keyframe(payload: Uint8Array): boolean | null {\n if (payload.length < 5) return null;\n\n let pos = 0;\n let iterations = 0;\n while (pos + 4 < payload.length) {\n if (++iterations > MAX_NAL_UNITS_PER_AU) return null;\n\n // Read 4-byte big-endian NAL unit length\n const nalLen = (payload[pos]! << 24) | (payload[pos + 1]! << 16) |\n (payload[pos + 2]! << 8) | payload[pos + 3]!;\n pos += 4;\n\n if (nalLen <= 0 || pos >= payload.length) break;\n\n const nalType = payload[pos]! & 0x1F;\n\n // Keyframe indicators\n if (nalType === 5) return true; // IDR slice\n if (nalType === 7) return true; // SPS (always precedes IDR in keyframe AU)\n\n // Non-VCL NALs to skip (may precede the actual slice)\n if (nalType === 6 || nalType === 8 || nalType === 9) {\n // SEI (6), PPS (8), AUD (9) — skip to next NAL\n pos += nalLen;\n continue;\n }\n\n // Any other VCL NAL type = delta frame\n return false;\n }\n\n return null; // Couldn't find a VCL NAL\n}\n\n/**\n * H.265: check for IDR_W_RADL (type 19) or IDR_N_LP (type 20).\n *\n * H.265 NAL header is 2 bytes:\n * forbidden_zero_bit(1) + nal_unit_type(6) + nuh_layer_id(6) + nuh_temporal_id_plus1(3)\n * Type is in bits 1-6 of the first byte: (byte >> 1) & 0x3F\n *\n * @see ITU-T H.265 §7.4.2.2 (NAL unit header semantics)\n */\nfunction isH265Keyframe(payload: Uint8Array): boolean | null {\n if (payload.length < 6) return null;\n\n let pos = 0;\n let iterations = 0;\n while (pos + 4 < payload.length) {\n if (++iterations > MAX_NAL_UNITS_PER_AU) return null;\n\n const nalLen = (payload[pos]! << 24) | (payload[pos + 1]! << 16) |\n (payload[pos + 2]! << 8) | payload[pos + 3]!;\n pos += 4;\n\n if (nalLen <= 0 || pos + 1 >= payload.length) break;\n\n const nalType = (payload[pos]! >> 1) & 0x3F;\n\n // IDR types\n if (nalType === 19 || nalType === 20) return true; // IDR_W_RADL, IDR_N_LP\n if (nalType === 21) return true; // CRA_NUT (clean random access)\n\n // VPS (32), SPS (33), PPS (34) — parameter sets precede IDR\n if (nalType === 32 || nalType === 33 || nalType === 34) {\n pos += nalLen;\n continue;\n }\n\n // SEI (39, 40), AUD (35) — skip\n if (nalType === 35 || nalType === 39 || nalType === 40) {\n pos += nalLen;\n continue;\n }\n\n // Any other slice type = delta\n return false;\n }\n\n return null;\n}\n\n/**\n * AV1: check for key_frame by looking for Sequence Header OBU.\n *\n * AV1 OBU header: forbidden(1) + obu_type(4) + extension_flag(1) + has_size_field(1) + reserved(1)\n * obu_type is bits 4-7: (byte >> 3) & 0x0F\n *\n * A keyframe access unit starts with OBU_SEQUENCE_HEADER (type 1).\n * A delta frame starts with OBU_FRAME (type 6) without a preceding sequence header.\n *\n * @see AV1 §5.3.1 (OBU header syntax)\n */\nfunction isAV1Keyframe(payload: Uint8Array): boolean | null {\n if (payload.length < 2) return null;\n\n const obuType = (payload[0]! >> 3) & 0x0F;\n\n // Sequence header = keyframe (always precedes key frame in a temporal unit)\n if (obuType === 1) return true; // OBU_SEQUENCE_HEADER\n\n // Temporal delimiter (type 2) — look at next OBU\n if (obuType === 2 && payload.length >= 3) {\n const nextObuType = (payload[2]! >> 3) & 0x0F;\n if (nextObuType === 1) return true;\n }\n\n // Frame or frame header without sequence header = delta\n if (obuType === 3 || obuType === 6) return false; // OBU_FRAME_HEADER, OBU_FRAME\n\n return null; // Unknown OBU type first — can't determine\n}\n","/**\n * QUIC variable-length integer encoding.\n * @see RFC 9000 §16\n * @see draft-ietf-moq-transport-16 §1.4\n * @module\n */\n\n/** Maximum value representable as a QUIC varint: 2^62 - 1 */\nexport const MAX_VARINT = 4611686018427387903n as Varint; // 2n**62n - 1n\n\ndeclare const VARINT_BRAND: unique symbol;\n\n/**\n * A branded bigint guaranteed to be a valid QUIC varint value:\n * non-negative integer ≤ MAX_VARINT (2^62 - 1).\n */\nexport type Varint = bigint & { readonly [VARINT_BRAND]: never };\n\n/**\n * Validate and create a branded Varint value.\n * Accepts both number and bigint inputs.\n * @throws {RangeError} if value is negative, non-integer, or > MAX_VARINT\n */\nexport function varint(n: number | bigint): Varint {\n if (typeof n === 'number') {\n if (!Number.isInteger(n) || n < 0) {\n throw new RangeError(\n `Varint value must be a non-negative integer, got ${n}`,\n );\n }\n if (n > Number.MAX_SAFE_INTEGER) {\n throw new RangeError(\n `Varint number value ${n} exceeds MAX_SAFE_INTEGER; use bigint for values > 2^53-1`,\n );\n }\n return BigInt(n) as Varint;\n }\n // bigint path\n if (n < 0n || n > MAX_VARINT) {\n throw new RangeError(\n `Varint value must be 0 ≤ n ≤ ${MAX_VARINT}, got ${n}`,\n );\n }\n return n as Varint;\n}\n\n/**\n * Determine the wire encoding length of a varint value.\n *\n * Accepts a raw `bigint` (not just a branded {@link Varint}) so that message\n * fields widened to `bigint` for draft-18 (full uint64) can pass through the\n * draft-14/16 QUIC encoders — but a value outside the QUIC range is rejected\n * here rather than silently truncated, which is what keeps a draft-14/16 message\n * from ever encoding a value > 2^62-1.\n *\n * @see RFC 9000 §16 — first 2 bits encode length\n * @throws {RangeError} if value is negative or exceeds MAX_VARINT (2^62-1).\n */\nexport function varintEncodingLength(value: bigint): 1 | 2 | 4 | 8 {\n if (value < 0n || value > MAX_VARINT) {\n throw new RangeError(`QUIC varint value out of range (0..${MAX_VARINT}): ${value}`);\n }\n if (value <= 63n) return 1;\n if (value <= 16383n) return 2;\n if (value <= 1073741823n) return 4;\n return 8;\n}\n\n/**\n * Read a varint from a buffer at the given offset.\n * @see RFC 9000 §16\n * @throws {RangeError} if buffer is too short\n */\nexport function readVarint(\n buf: Uint8Array,\n offset: number,\n): { value: Varint; bytesRead: number } {\n if (offset >= buf.length) {\n throw new RangeError(`Varint read out of bounds: offset ${offset}, length ${buf.length}`);\n }\n\n const first = buf[offset]!;\n const lengthFlag = first >> 6; // first 2 bits\n const bytesRead = (1 << lengthFlag) as 1 | 2 | 4 | 8;\n\n if (offset + bytesRead > buf.length) {\n throw new RangeError(\n `Varint requires ${bytesRead} bytes but only ${buf.length - offset} available`,\n );\n }\n\n let value: bigint;\n\n switch (bytesRead) {\n case 1:\n value = BigInt(first & 0x3f);\n break;\n case 2:\n value = BigInt(((first & 0x3f) << 8) | buf[offset + 1]!);\n break;\n case 4:\n value = BigInt(\n (((first & 0x3f) << 24) |\n (buf[offset + 1]! << 16) |\n (buf[offset + 2]! << 8) |\n buf[offset + 3]!) >>> 0,\n );\n break;\n case 8: {\n // Assemble all 8 bytes into a single bigint, masking the length flag bits\n value =\n (BigInt(first & 0x3f) << 56n) |\n (BigInt(buf[offset + 1]!) << 48n) |\n (BigInt(buf[offset + 2]!) << 40n) |\n (BigInt(buf[offset + 3]!) << 32n) |\n (BigInt(buf[offset + 4]!) << 24n) |\n (BigInt(buf[offset + 5]!) << 16n) |\n (BigInt(buf[offset + 6]!) << 8n) |\n BigInt(buf[offset + 7]!);\n break;\n }\n }\n\n return { value: value! as Varint, bytesRead };\n}\n\n/**\n * Write a varint to a buffer at the given offset.\n *\n * Accepts a raw `bigint`; out-of-QUIC-range values throw via\n * {@link varintEncodingLength} rather than being silently truncated.\n *\n * @see RFC 9000 §16\n * @returns Number of bytes written.\n * @throws {RangeError} if value is negative or exceeds MAX_VARINT (2^62-1).\n */\nexport function writeVarint(\n value: bigint,\n buf: Uint8Array,\n offset: number,\n): number {\n const len = varintEncodingLength(value);\n\n switch (len) {\n case 1:\n buf[offset] = Number(value); // top 2 bits are 00\n return 1;\n case 2:\n buf[offset] = 0x40 | Number(value >> 8n);\n buf[offset + 1] = Number(value & 0xffn);\n return 2;\n case 4: {\n buf[offset] = 0x80 | Number((value >> 24n) & 0x3fn);\n buf[offset + 1] = Number((value >> 16n) & 0xffn);\n buf[offset + 2] = Number((value >> 8n) & 0xffn);\n buf[offset + 3] = Number(value & 0xffn);\n return 4;\n }\n case 8: {\n buf[offset] = 0xc0 | Number((value >> 56n) & 0x3fn);\n buf[offset + 1] = Number((value >> 48n) & 0xffn);\n buf[offset + 2] = Number((value >> 40n) & 0xffn);\n buf[offset + 3] = Number((value >> 32n) & 0xffn);\n buf[offset + 4] = Number((value >> 24n) & 0xffn);\n buf[offset + 5] = Number((value >> 16n) & 0xffn);\n buf[offset + 6] = Number((value >> 8n) & 0xffn);\n buf[offset + 7] = Number(value & 0xffn);\n return 8;\n }\n }\n}\n","/**\n * draft-18 wire code tables.\n *\n * Distinct from the draft-16 tables (`./codes.ts`, `./parameters.ts`) because\n * draft-18 renumbers and adds/removes several codes: a unified SETUP (0x2F00)\n * replaces CLIENT_SETUP/SERVER_SETUP; SUBSCRIBE_NAMESPACE splits into 0x50 +\n * SUBSCRIBE_TRACKS 0x51; UNSUBSCRIBE/FETCH_CANCEL/MAX_REQUEST_ID/REQUESTS_BLOCKED\n * are removed; PUBLISH_BLOCKED 0x0F is new. All values verified against the\n * draft-18 IANA registries.\n *\n * Values are plain numbers; on the wire they are encoded with vi64\n * (`../primitives/vi64.ts`), so the multi-byte SETUP type (0x2F00) is fine.\n *\n * @see draft-ietf-moq-transport-18 §16 (IANA), §3.4 (stream types), §11.4 (data)\n * @module\n */\n\n/**\n * draft-18 control message type codes (§16 message type registry).\n * Removed in draft-18: CLIENT_SETUP, SERVER_SETUP, UNSUBSCRIBE, FETCH_CANCEL,\n * MAX_REQUEST_ID, REQUESTS_BLOCKED, PUBLISH_NAMESPACE_DONE, PUBLISH_NAMESPACE_CANCEL.\n *\n * PUBLISH_OK: a PUBLISH is accepted with the REQUEST_OK shorthand (wire type\n * 0x07), per §10.5; the standalone 0x1E PUBLISH_OK message of draft-14/16 was\n * removed in draft-18 (changelog). So this table has no 0x1E entry — the codec\n * neither encodes nor decodes a draft-18 PUBLISH_OK, and a 0x1E on a draft-18\n * control stream is rejected as an unknown type. (The draft-18 spec's message\n * table still lists 0x1E PUBLISH_OK; we treat §10.5 + the changelog as\n * authoritative and intentionally do not implement it.)\n */\nexport const ControlMessageType18 = {\n REQUEST_UPDATE: 0x02,\n SUBSCRIBE: 0x03,\n SUBSCRIBE_OK: 0x04,\n REQUEST_ERROR: 0x05,\n PUBLISH_NAMESPACE: 0x06,\n REQUEST_OK: 0x07,\n NAMESPACE: 0x08,\n PUBLISH_DONE: 0x0b,\n TRACK_STATUS: 0x0d,\n NAMESPACE_DONE: 0x0e,\n PUBLISH_BLOCKED: 0x0f,\n GOAWAY: 0x10,\n FETCH: 0x16,\n FETCH_OK: 0x18,\n PUBLISH: 0x1d,\n /** draft-22 §9.10 only: a publisher's report of its subscription state. */\n PUBLISH_STATE_NOTIFY: 0x22,\n // PUBLISH_OK (0x1E) intentionally omitted — see the note above (REQUEST_OK shorthand).\n SUBSCRIBE_NAMESPACE: 0x50,\n SUBSCRIBE_TRACKS: 0x51,\n /** Unified setup message; also the control-stream stream type. */\n SETUP: 0x2f00,\n} as const;\n\n/**\n * draft-18 Setup Option codes (§10.3.1). KVP-encoded, no count, span the payload.\n * MAX_REQUEST_ID (draft-16 0x02) is removed.\n */\nexport const SetupOption18 = {\n PATH: 0x01,\n AUTHORIZATION_TOKEN: 0x03,\n MAX_AUTH_TOKEN_CACHE_SIZE: 0x04,\n AUTHORITY: 0x05,\n MOQT_IMPLEMENTATION: 0x07,\n} as const;\n\n/** draft-22 Setup Options added to the draft-18 set (§9.1.6, §9.1.7). */\nexport const SetupOption22 = {\n ...SetupOption18,\n MAX_FILTER_RANGES: 0x06,\n MAX_REQUEST_UPDATES: 0x08,\n} as const;\n\n/**\n * Value-encoding kind for a draft-18 message parameter (§10.2).\n * `namespace` is a Track Namespace structure (§2.4.1): a vi64 field count\n * followed by that many vi64-length-prefixed fields (used by TRACK_NAMESPACE_PREFIX).\n */\n/**\n * `locationFilter` is draft 22's LOCATION_FILTER (§9.20.9): a Location Filter\n * Type followed by the 0-4 vi64 fields it selects, with no Length.\n */\nexport type ParamValueKind = 'uint8' | 'varint' | 'location' | 'bytes' | 'namespace' | 'locationFilter';\n\n/**\n * draft-18 Message Parameter codes + their value encodings (§10.2 registry).\n * Message parameters use Type-Delta (vi64) + a per-type value (NOT KVP parity).\n * DELIVERY_TIMEOUT splits into OBJECT_/SUBGROUP_; RENDEZVOUS_/FILL_TIMEOUT and\n * TRACK_NAMESPACE_PREFIX are new.\n */\nexport const MessageParam18: Record<string, { type: number; kind: ParamValueKind }> = {\n OBJECT_DELIVERY_TIMEOUT: { type: 0x02, kind: 'varint' },\n AUTHORIZATION_TOKEN: { type: 0x03, kind: 'bytes' },\n RENDEZVOUS_TIMEOUT: { type: 0x04, kind: 'varint' },\n SUBGROUP_DELIVERY_TIMEOUT: { type: 0x06, kind: 'varint' },\n EXPIRES: { type: 0x08, kind: 'varint' },\n LARGEST_OBJECT: { type: 0x09, kind: 'location' },\n FILL_TIMEOUT: { type: 0x0a, kind: 'varint' },\n FORWARD: { type: 0x10, kind: 'uint8' },\n SUBSCRIBER_PRIORITY: { type: 0x20, kind: 'uint8' },\n SUBSCRIPTION_FILTER: { type: 0x21, kind: 'bytes' },\n GROUP_ORDER: { type: 0x22, kind: 'uint8' },\n NEW_GROUP_REQUEST: { type: 0x32, kind: 'varint' },\n TRACK_NAMESPACE_PREFIX: { type: 0x34, kind: 'namespace' },\n} as const;\n\n/**\n * draft-22 Message Parameter codes + value encodings. Draft 18's table with 0x21\n * renamed LOCATION_FILTER (a typed Location filter with no Length, §9.20.9) and\n * FILL_PARAMETERS (0x23), the Range Filters (0x25-0x29) and INCLUDE_PROPERTIES\n * (0x35) added.\n */\nexport const MessageParam22: Record<string, { type: number; kind: ParamValueKind }> = {\n OBJECT_DELIVERY_TIMEOUT: { type: 0x02, kind: 'varint' },\n AUTHORIZATION_TOKEN: { type: 0x03, kind: 'bytes' },\n RENDEZVOUS_TIMEOUT: { type: 0x04, kind: 'varint' },\n SUBGROUP_DELIVERY_TIMEOUT: { type: 0x06, kind: 'varint' },\n EXPIRES: { type: 0x08, kind: 'varint' },\n LARGEST_OBJECT: { type: 0x09, kind: 'location' },\n FILL_TIMEOUT: { type: 0x0a, kind: 'varint' },\n FORWARD: { type: 0x10, kind: 'uint8' },\n SUBSCRIBER_PRIORITY: { type: 0x20, kind: 'uint8' },\n LOCATION_FILTER: { type: 0x21, kind: 'locationFilter' },\n GROUP_ORDER: { type: 0x22, kind: 'uint8' },\n FILL_PARAMETERS: { type: 0x23, kind: 'bytes' },\n SUBGROUP_FILTER: { type: 0x25, kind: 'bytes' },\n OBJECTID_FILTER: { type: 0x26, kind: 'bytes' },\n PRIORITY_FILTER: { type: 0x27, kind: 'bytes' },\n OBJECT_PROPERTY_FILTER: { type: 0x28, kind: 'bytes' },\n TRACK_PROPERTY_FILTER: { type: 0x29, kind: 'bytes' },\n NEW_GROUP_REQUEST: { type: 0x32, kind: 'varint' },\n TRACK_NAMESPACE_PREFIX: { type: 0x34, kind: 'namespace' },\n INCLUDE_PROPERTIES: { type: 0x35, kind: 'uint8' },\n} as const;\n\n/** Property type ranges (§2.5, §12). Properties are carried by control messages\n * (Track Properties) and data objects (Object Properties); the type-space is shared. */\nexport const PropertyRange18 = {\n /** Immutable Properties container (Track scope), added by original publisher only. */\n IMMUTABLE: 0x0b,\n /** Mandatory Track Properties: must be understood or refuse with UNSUPPORTED_EXTENSION. */\n MANDATORY_MIN: 0x4000,\n MANDATORY_MAX: 0x7fff,\n} as const;\n\n/** Whether a property type is a Mandatory Track Property (0x4000–0x7FFF, §2.5.1). */\nexport function isMandatoryProperty(type: number): boolean {\n return type >= PropertyRange18.MANDATORY_MIN && type <= PropertyRange18.MANDATORY_MAX;\n}\n","/**\n * MOQT error code registries.\n * @see draft-ietf-moq-transport-16 §13.4\n * @module\n */\n\nimport { varint } from './primitives/varint.js';\n\n/**\n * Session Termination Error Codes.\n * @see draft-ietf-moq-transport-16 §13.4.1\n */\nexport const SessionError = {\n NO_ERROR: varint(0x0),\n INTERNAL_ERROR: varint(0x1),\n UNAUTHORIZED: varint(0x2),\n PROTOCOL_VIOLATION: varint(0x3),\n INVALID_REQUEST_ID: varint(0x4),\n DUPLICATE_TRACK_ALIAS: varint(0x5),\n KEY_VALUE_FORMATTING_ERROR: varint(0x6),\n TOO_MANY_REQUESTS: varint(0x7),\n INVALID_PATH: varint(0x8),\n MALFORMED_PATH: varint(0x9),\n GOAWAY_TIMEOUT: varint(0x10),\n CONTROL_MESSAGE_TIMEOUT: varint(0x11),\n DATA_STREAM_TIMEOUT: varint(0x12),\n AUTH_TOKEN_CACHE_OVERFLOW: varint(0x13),\n DUPLICATE_AUTH_TOKEN_ALIAS: varint(0x14),\n VERSION_NEGOTIATION_FAILED: varint(0x15),\n MALFORMED_AUTH_TOKEN: varint(0x16),\n UNKNOWN_AUTH_TOKEN_ALIAS: varint(0x17),\n EXPIRED_AUTH_TOKEN: varint(0x18),\n INVALID_AUTHORITY: varint(0x19),\n MALFORMED_AUTHORITY: varint(0x1a),\n /** draft-22: more than MAX_REQUEST_UPDATES updates outstanding on one request. */\n TOO_MANY_REQUEST_UPDATES: varint(0x1b),\n} as const;\n\n/**\n * REQUEST_ERROR codes.\n * @see draft-ietf-moq-transport-16 §13.4.2\n */\nexport const RequestError = {\n INTERNAL_ERROR: varint(0x0),\n UNAUTHORIZED: varint(0x1),\n TIMEOUT: varint(0x2),\n NOT_SUPPORTED: varint(0x3),\n MALFORMED_AUTH_TOKEN: varint(0x4),\n EXPIRED_AUTH_TOKEN: varint(0x5),\n DOES_NOT_EXIST: varint(0x10),\n INVALID_RANGE: varint(0x11),\n MALFORMED_TRACK: varint(0x12),\n DUPLICATE_SUBSCRIPTION: varint(0x19),\n UNINTERESTED: varint(0x20),\n PREFIX_OVERLAP: varint(0x30),\n NAMESPACE_TOO_LARGE: varint(0x31),\n INVALID_JOINING_REQUEST_ID: varint(0x32),\n /** draft-18 §2.5.1: a Mandatory Track Property the endpoint does not understand. */\n UNSUPPORTED_EXTENSION: varint(0x33),\n /** draft-18 §10.6.2: the request is redirected; the body carries a Redirect. */\n REDIRECT: varint(0x34),\n} as const;\n\n/**\n * PUBLISH_DONE Status Codes — **draft-14/16** (legacy).\n *\n * NOTE: draft-18 renumbers this table — EXPIRED and TOO_FAR_BEHIND are swapped\n * (0x5↔0x6) and EXCESSIVE_LOAD (0x9) is added. Use {@link PublishDoneCode18} for\n * draft-18; this export is retained unchanged for draft-14/16 compatibility.\n * @see draft-ietf-moq-transport-16 §13.4.3\n */\nexport const PublishDoneCode = {\n INTERNAL_ERROR: varint(0x0),\n UNAUTHORIZED: varint(0x1),\n TRACK_ENDED: varint(0x2),\n SUBSCRIPTION_ENDED: varint(0x3),\n GOING_AWAY: varint(0x4),\n EXPIRED: varint(0x5),\n TOO_FAR_BEHIND: varint(0x6),\n UPDATE_FAILED: varint(0x8),\n MALFORMED_TRACK: varint(0x12),\n} as const;\n\n/**\n * Data Stream Reset Error Codes — **draft-14/16** (legacy).\n *\n * NOTE: draft-18 renumbers this table (see {@link StreamResetCode18}) —\n * UNKNOWN_OBJECT_STATUS moves from 0x4 to 0x6, and 0x4 becomes GOING_AWAY. This\n * export is retained unchanged for draft-14/16 compatibility.\n * @see draft-ietf-moq-transport-16 §13.4.4\n */\nexport const DataStreamError = {\n INTERNAL_ERROR: varint(0x0),\n CANCELLED: varint(0x1),\n DELIVERY_TIMEOUT: varint(0x2),\n SESSION_CLOSED: varint(0x3),\n UNKNOWN_OBJECT_STATUS: varint(0x4),\n MALFORMED_TRACK: varint(0x12),\n} as const;\n\n// ─── draft-18 error code registries (canonical) ──────────────────────────────\n// draft-18 renumbers several registries; these are the authoritative draft-18\n// tables. The legacy exports above stay frozen for draft-14/16. Consumers select\n// the registry by the negotiated draft version.\n\n/**\n * REQUEST_ERROR codes — **draft-18** (§15.10.2). Superset of the legacy table:\n * adds GOING_AWAY (0x6) and EXCESSIVE_LOAD (0x9).\n */\nexport const RequestError18 = {\n INTERNAL_ERROR: varint(0x0),\n UNAUTHORIZED: varint(0x1),\n TIMEOUT: varint(0x2),\n NOT_SUPPORTED: varint(0x3),\n MALFORMED_AUTH_TOKEN: varint(0x4),\n EXPIRED_AUTH_TOKEN: varint(0x5),\n GOING_AWAY: varint(0x6),\n EXCESSIVE_LOAD: varint(0x9),\n DOES_NOT_EXIST: varint(0x10),\n INVALID_RANGE: varint(0x11),\n MALFORMED_TRACK: varint(0x12),\n DUPLICATE_SUBSCRIPTION: varint(0x19),\n UNINTERESTED: varint(0x20),\n PREFIX_OVERLAP: varint(0x30),\n NAMESPACE_TOO_LARGE: varint(0x31),\n INVALID_JOINING_REQUEST_ID: varint(0x32),\n UNSUPPORTED_EXTENSION: varint(0x33),\n REDIRECT: varint(0x34),\n} as const;\n\n/**\n * REQUEST_ERROR codes — **draft-22** (§12.3): draft 18's without\n * DUPLICATE_SUBSCRIPTION and INVALID_JOINING_REQUEST_ID, plus the filter codes.\n */\nexport const RequestError22 = {\n INTERNAL_ERROR: varint(0x0),\n UNAUTHORIZED: varint(0x1),\n TIMEOUT: varint(0x2),\n NOT_SUPPORTED: varint(0x3),\n MALFORMED_AUTH_TOKEN: varint(0x4),\n EXPIRED_AUTH_TOKEN: varint(0x5),\n GOING_AWAY: varint(0x6),\n EXCESSIVE_LOAD: varint(0x9),\n DOES_NOT_EXIST: varint(0x10),\n INVALID_RANGE: varint(0x11),\n MALFORMED_TRACK: varint(0x12),\n UNINTERESTED: varint(0x20),\n PREFIX_OVERLAP: varint(0x30),\n NAMESPACE_TOO_LARGE: varint(0x31),\n UNSUPPORTED_EXTENSION: varint(0x33),\n REDIRECT: varint(0x34),\n CONFLICTING_FILTERS: varint(0x35),\n INVALID_FILTER: varint(0x36),\n} as const;\n\n/**\n * PUBLISH_DONE Status Codes — **draft-18** (§15.10.3). Differs from legacy:\n * TOO_FAR_BEHIND = 0x5 and EXPIRED = 0x6 (swapped), plus EXCESSIVE_LOAD (0x9).\n */\nexport const PublishDoneCode18 = {\n INTERNAL_ERROR: varint(0x0),\n UNAUTHORIZED: varint(0x1),\n TRACK_ENDED: varint(0x2),\n SUBSCRIPTION_ENDED: varint(0x3),\n GOING_AWAY: varint(0x4),\n TOO_FAR_BEHIND: varint(0x5),\n EXPIRED: varint(0x6),\n UPDATE_FAILED: varint(0x8),\n EXCESSIVE_LOAD: varint(0x9),\n MALFORMED_TRACK: varint(0x12),\n} as const;\n\n/**\n * Stream Reset Error Codes — **draft-18** (§15.10.4), the canonical draft-18 name\n * for what draft-14/16 called {@link DataStreamError}. Differs from legacy:\n * GOING_AWAY = 0x4, TOO_FAR_BEHIND = 0x5, UNKNOWN_OBJECT_STATUS moves to 0x6, plus\n * EXPIRED_AUTH_TOKEN (0x7) and EXCESSIVE_LOAD (0x9).\n */\nexport const StreamResetCode18 = {\n INTERNAL_ERROR: varint(0x0),\n CANCELLED: varint(0x1),\n DELIVERY_TIMEOUT: varint(0x2),\n SESSION_CLOSED: varint(0x3),\n GOING_AWAY: varint(0x4),\n TOO_FAR_BEHIND: varint(0x5),\n UNKNOWN_OBJECT_STATUS: varint(0x6),\n EXPIRED_AUTH_TOKEN: varint(0x7),\n EXCESSIVE_LOAD: varint(0x9),\n MALFORMED_TRACK: varint(0x12),\n} as const;\n\n/** @deprecated Use {@link StreamResetCode18} — the canonical draft-18 name. */\nexport const DataStreamError18 = StreamResetCode18;\n\n// ─── Typed Error Classes ──────────────────────────────────────────────\n\n/**\n * Thrown when a MOQT protocol violation is detected that requires\n * the session to be closed with PROTOCOL_VIOLATION (0x3).\n *\n * Used by decoders and validators to signal violations that the\n * I/O layer (adapter) must convert into session closure. Using a\n * typed class instead of string matching ensures reliable detection.\n *\n * @see draft-ietf-moq-transport-16 §9 (malformed control messages)\n * @see draft-ietf-moq-transport-16 §10.4 (data stream violations)\n * @see draft-ietf-moq-transport-16 §10.3.1 (datagram violations)\n */\nexport class ProtocolViolationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ProtocolViolationError';\n }\n}\n","/**\n * Low-level binary read/write helpers for MOQT wire format.\n * @see draft-ietf-moq-transport-16\n * @module\n */\n\nimport { readVarint, writeVarint, varint, varintEncodingLength } from './varint.js';\nimport { ProtocolViolationError } from '../errors.js';\n\n/** Read a single byte. */\nexport function readUint8(buf: Uint8Array, offset: number): { value: number; bytesRead: 1 } {\n if (offset >= buf.length) {\n throw new RangeError(`readUint8 out of bounds: offset ${offset}, length ${buf.length}`);\n }\n return { value: buf[offset]!, bytesRead: 1 };\n}\n\n/** Write a single byte. Returns 1 (bytes written). */\nexport function writeUint8(value: number, buf: Uint8Array, offset: number): 1 {\n buf[offset] = value & 0xff;\n return 1;\n}\n\n/** Read a fixed number of bytes as a copy. */\nexport function readBytes(\n buf: Uint8Array,\n offset: number,\n length: number,\n): { value: Uint8Array; bytesRead: number } {\n if (offset + length > buf.length) {\n throw new RangeError(\n `readBytes: need ${length} bytes at offset ${offset}, but buffer length is ${buf.length}`,\n );\n }\n const value = buf.slice(offset, offset + length);\n return { value, bytesRead: length };\n}\n\n/** Read a varint-length-prefixed byte array. */\nexport function readLengthPrefixedBytes(\n buf: Uint8Array,\n offset: number,\n): { value: Uint8Array; bytesRead: number } {\n const { value: length, bytesRead: lenBytes } = readVarint(buf, offset);\n const numLen = Number(length);\n const { value, bytesRead: dataBytes } = readBytes(buf, offset + lenBytes, numLen);\n return { value, bytesRead: lenBytes + dataBytes };\n}\n\n/** Write a varint-length-prefixed byte array. Returns total bytes written. */\nexport function writeLengthPrefixedBytes(\n data: Uint8Array,\n buf: Uint8Array,\n offset: number,\n): number {\n const v = varint(data.length);\n let pos = offset;\n pos += writeVarint(v, buf, pos);\n buf.set(data, pos);\n pos += data.length;\n return pos - offset;\n}\n\n/** Calculate total encoding length for a length-prefixed byte array. */\nexport function lengthPrefixedBytesEncodingLength(data: Uint8Array): number {\n return varintEncodingLength(varint(data.length)) + data.length;\n}\n\n/**\n * Read a varint-count-prefixed tuple of length-prefixed byte segments.\n * Used for Track Namespace (array of byte-string segments).\n * @see draft-ietf-moq-transport-16 §9 (Track Namespace Tuple)\n */\nexport function readTuple(\n buf: Uint8Array,\n offset: number,\n): { value: Uint8Array[]; bytesRead: number } {\n let pos = offset;\n const { value: count, bytesRead: countBytes } = readVarint(buf, pos);\n pos += countBytes;\n\n const numCount = Number(count);\n const segments: Uint8Array[] = [];\n for (let i = 0; i < numCount; i++) {\n const { value: segment, bytesRead: segBytes } = readLengthPrefixedBytes(buf, pos);\n segments.push(segment);\n pos += segBytes;\n }\n\n return { value: segments, bytesRead: pos - offset };\n}\n\n/**\n * Write a varint-count-prefixed tuple of length-prefixed byte segments.\n * Returns total bytes written.\n */\nexport function writeTuple(\n segments: Uint8Array[],\n buf: Uint8Array,\n offset: number,\n): number {\n let pos = offset;\n pos += writeVarint(varint(segments.length), buf, pos);\n for (const seg of segments) {\n pos += writeLengthPrefixedBytes(seg, buf, pos);\n }\n return pos - offset;\n}\n\n/** Calculate encoding length for a tuple. */\nexport function tupleEncodingLength(segments: Uint8Array[]): number {\n let len = varintEncodingLength(varint(segments.length));\n for (const seg of segments) {\n len += lengthPrefixedBytesEncodingLength(seg);\n }\n return len;\n}\n\n// ─── Track Namespace Validation (§2.4.1) ─────────────────────────────\n\n/** Maximum number of Track Namespace Fields per §2.4.1. */\nconst MAX_NAMESPACE_FIELDS = 32;\n\n/** Maximum total length of Track Namespace or Full Track Name per §2.4.1. */\nconst MAX_NAME_LENGTH = 4096;\n\n/**\n * Calculate total byte length of namespace fields (sum of field lengths).\n */\nfunction namespaceByteLength(segments: Uint8Array[]): number {\n return segments.reduce((sum, seg) => sum + seg.length, 0);\n}\n\n/**\n * Validate field lengths - each must be ≥ 1 byte per §2.4.1.\n * @throws {Error} If any field has length 0\n */\nfunction validateFieldLengths(segments: Uint8Array[]): void {\n for (let i = 0; i < segments.length; i++) {\n if (segments[i]!.length === 0) {\n throw new ProtocolViolationError(\n ` Track Namespace Field ${i} has length 0; each field must contain at least one byte (§2.4.1)`,\n );\n }\n }\n}\n\n/**\n * Validate a Track Namespace (full namespace, not prefix).\n *\n * Field-count lower bound is version-specific: draft-18 §2.4.1 allows a Track\n * Namespace of **0 to 32** fields (an empty full namespace is legal), so callers\n * on draft-18 pass `{ allowEmpty: true }`. draft-14/16 require at least one field\n * — the default — so legacy callers are unchanged.\n *\n * @param segments Array of namespace field byte arrays\n * @param opts.allowEmpty Permit a zero-field namespace (draft-18 §2.4.1).\n * @throws {ProtocolViolationError} If field count is out of range, any field is\n * empty, or the total exceeds 4096 bytes.\n * @see draft-ietf-moq-transport-18 §2.4.1\n */\nexport function validateTrackNamespace(\n segments: Uint8Array[],\n opts: { allowEmpty?: boolean } = {},\n): void {\n // §2.4.1: draft-18 allows 0 fields; draft-14/16 require ≥ 1.\n if (segments.length === 0 && !opts.allowEmpty) {\n throw new ProtocolViolationError(\n ' Track Namespace must have at least 1 field (§2.4.1)',\n );\n }\n if (segments.length > MAX_NAMESPACE_FIELDS) {\n throw new ProtocolViolationError(\n ` Track Namespace has ${segments.length} fields; maximum is 32 (§2.4.1)`,\n );\n }\n\n // Each field must have at least 1 byte\n validateFieldLengths(segments);\n\n // Total length must be ≤ 4096\n const totalLength = namespaceByteLength(segments);\n if (totalLength > MAX_NAME_LENGTH) {\n throw new ProtocolViolationError(\n ` Track Namespace length ${totalLength} exceeds maximum 4096 bytes (§2.4.1)`,\n );\n }\n}\n\n/** The `.session` reserved first-field value: bytes of the ASCII string \".session\". */\nconst SESSION_NAMESPACE_FIELD = new Uint8Array([0x2e, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e]);\n\n/** True if a namespace's first field equals `expected` (exact byte comparison). */\nfunction firstFieldEquals(segments: Uint8Array[], expected: Uint8Array): boolean {\n const first = segments[0];\n if (first === undefined || first.length !== expected.length) return false;\n for (let i = 0; i < expected.length; i++) {\n if (first[i] !== expected[i]) return false;\n }\n return true;\n}\n\n/**\n * §3.2.2: whether a Track Namespace is in the reserved `.session` namespace —\n * its first field is exactly `.session`. Such tracks/namespaces are managed by\n * the MOQT implementation; the Application MUST NOT publish under them.\n * @see draft-ietf-moq-transport-18 §3.2.2\n */\nexport function isReservedSessionNamespace(segments: Uint8Array[]): boolean {\n return firstFieldEquals(segments, SESSION_NAMESPACE_FIELD);\n}\n\n/**\n * §3.2.1: whether a Track Namespace's first field is exactly a single period\n * (`.`, 0x2e). This value is reserved and MUST NOT be used for any purpose.\n * @see draft-ietf-moq-transport-18 §3.2.1\n */\nexport function isReservedDotNamespace(segments: Uint8Array[]): boolean {\n const first = segments[0];\n return first !== undefined && first.length === 1 && first[0] === 0x2e;\n}\n\n/**\n * Validate a Track Namespace Prefix (allows 0 fields for SUBSCRIBE_NAMESPACE).\n * @param segments Array of namespace field byte arrays\n * @throws {Error} If field count > 32, any field is empty, or total > 4096 bytes\n * @see draft-ietf-moq-transport-16 §9.25\n */\nexport function validateTrackNamespacePrefix(segments: Uint8Array[]): void {\n // Prefix allows 0-32 fields\n if (segments.length > MAX_NAMESPACE_FIELDS) {\n throw new ProtocolViolationError(\n ` Track Namespace Prefix has ${segments.length} fields; maximum is 32 (§9.25)`,\n );\n }\n\n // Each field (if present) must have at least 1 byte\n validateFieldLengths(segments);\n\n // Total length must be ≤ 4096\n const totalLength = namespaceByteLength(segments);\n if (totalLength > MAX_NAME_LENGTH) {\n throw new ProtocolViolationError(\n ` Track Namespace Prefix length ${totalLength} exceeds maximum 4096 bytes (§2.4.1)`,\n );\n }\n}\n\n/**\n * Validate a Full Track Name (namespace + track name).\n *\n * The namespace must be valid (each field non-empty, ≤ 32 fields) and the combined\n * namespace + track-name byte length must be ≤ 4096 (§2.4.1). The namespace\n * field-count lower bound is version-specific: draft-14/16 require ≥ 1 field (the\n * default), while draft-18 §2.4.1 permits an empty namespace — pass\n * `{ allowEmptyNamespace: true }` there. Note that an *empty namespace* (0 fields)\n * is distinct from an *empty field* (a present field of length 0): the latter is\n * always a violation.\n *\n * @param namespace Array of namespace field byte arrays\n * @param trackName Track name byte array\n * @param opts.allowEmptyNamespace Permit a zero-field namespace (draft-18 §2.4.1).\n * @throws {ProtocolViolationError} If namespace invalid or combined length > 4096 bytes\n * @see draft-ietf-moq-transport-18 §2.4.1\n */\nexport function validateFullTrackName(\n namespace: Uint8Array[],\n trackName: Uint8Array,\n opts: { allowEmptyNamespace?: boolean } = {},\n): void {\n // Validate namespace itself (≤ 32 fields, each non-empty; empty namespace only\n // when explicitly allowed for draft-18).\n validateTrackNamespace(namespace, opts.allowEmptyNamespace ? { allowEmpty: true } : {});\n\n // Total = namespace field lengths + track name length\n const totalLength = namespaceByteLength(namespace) + trackName.length;\n if (totalLength > MAX_NAME_LENGTH) {\n throw new ProtocolViolationError(\n ` Full Track Name length ${totalLength} exceeds maximum 4096 bytes (§2.4.1)`,\n );\n }\n}\n\n/**\n * Validate a Track Namespace Suffix (for NAMESPACE, NAMESPACE_DONE).\n * Suffix can have 0+ fields, but must not exceed limits since combined\n * prefix + suffix must satisfy §2.4.1.\n * @param segments Array of namespace field byte arrays\n * @throws {Error} If field count > 32, any field is empty, or total > 4096 bytes\n * @see draft-ietf-moq-transport-16 §9.21, §9.23\n */\nexport function validateTrackNamespaceSuffix(segments: Uint8Array[]): void {\n // Suffix cannot exceed the maximum field count (since prefix has ≥0 fields)\n if (segments.length > MAX_NAMESPACE_FIELDS) {\n throw new ProtocolViolationError(\n ` Track Namespace Suffix has ${segments.length} fields; maximum is 32 (§2.4.1)`,\n );\n }\n\n // Each field (if present) must have at least 1 byte\n validateFieldLengths(segments);\n\n // Total length must be ≤ 4096 (since prefix length ≥ 0)\n const totalLength = namespaceByteLength(segments);\n if (totalLength > MAX_NAME_LENGTH) {\n throw new ProtocolViolationError(\n ` Track Namespace Suffix length ${totalLength} exceeds maximum 4096 bytes (§2.4.1)`,\n );\n }\n}\n","/**\n * draft-18 Message Parameter codec (§10.2).\n *\n * Replaces the draft-16 KVP encoding. The wire form is a vi64 count followed by\n * that many `Message Parameter { Type Delta (vi64), Value (..) }`, serialized in\n * ascending Type order (delta = thisType - prevType, prevType starts at 0). The\n * value encoding is fixed per parameter Type:\n * - uint8: a single byte\n * - varint: a vi64 ← NOT the QUIC varint\n * - location: two vi64 (group, object)\n * - bytes: a vi64 length followed by that many bytes\n *\n * Because every parameter must be understood, there is no skip mechanism: an\n * unknown Type is a PROTOCOL_VIOLATION, and so is a cumulative Type that exceeds\n * 2^64-1. Decoding therefore requires a registry mapping Type → value kind.\n *\n * @see draft-ietf-moq-transport-18 §10.2\n * @module\n */\n\nimport { readVi64, writeVi64, vi64EncodingLength, MAX_VI64 } from '../primitives/vi64.js';\nimport { readUint8, writeUint8, validateTrackNamespacePrefix } from '../primitives/bytes.js';\nimport { ProtocolViolationError } from '../errors.js';\nimport { MessageParam18, MessageParam22, type ParamValueKind } from './codes-18.js';\n\nexport type { ParamValueKind };\n\n/** A decoded/encodable message-parameter value, tagged by its wire encoding. */\nexport type MessageParamValue =\n | { readonly kind: 'uint8'; readonly value: number }\n | { readonly kind: 'varint'; readonly value: bigint }\n | { readonly kind: 'location'; readonly group: bigint; readonly object: bigint }\n | { readonly kind: 'bytes'; readonly value: Uint8Array }\n // Track Namespace structure (§2.4.1): vi64 field count + vi64-length-prefixed\n // fields. The semantic value is the field list (e.g. TRACK_NAMESPACE_PREFIX).\n | { readonly kind: 'namespace'; readonly value: Uint8Array[] }\n // draft-22 LOCATION_FILTER (§9.20.9): the Location Filter Type and its fields,\n // written as-is with no Length (the type delimits the value).\n | { readonly kind: 'locationFilter'; readonly value: Uint8Array };\n\n// ─── draft-22 LOCATION_FILTER (type + 0-4 vi64 fields, no Length) ─────\n\n/** Fields that follow each draft-22 Location Filter Type (0x00-0x05). */\nconst LOCATION_FILTER_FIELD_COUNTS = [0, 1, 2, 3, 4, 0] as const;\n\n/**\n * The byte length of the draft-22 LOCATION_FILTER value starting at `offset`.\n * @throws {ProtocolViolationError} on an unknown type or a truncated field.\n */\nexport function typedLocationFilterLength(buf: Uint8Array, offset: number): number {\n let p = offset;\n try {\n const type = readVi64(buf, p);\n p += type.bytesRead;\n if (type.value > 5n) {\n throw new ProtocolViolationError(`Unknown Location Filter Type 0x${type.value.toString(16)}`);\n }\n for (let i = 0; i < LOCATION_FILTER_FIELD_COUNTS[Number(type.value)]!; i++) {\n p += readVi64(buf, p).bytesRead;\n }\n } catch (err) {\n if (err instanceof ProtocolViolationError) throw err;\n throw new ProtocolViolationError('LOCATION_FILTER has a truncated field');\n }\n return p - offset;\n}\n\n// ─── Track Namespace tuple (vi64 count + vi64-length-prefixed fields) ──\n\n/**\n * Enforce the Track Namespace structural rules (§2.4.1) at the codec boundary:\n * every field is a Uint8Array, ≤32 fields, each field non-empty, total ≤4096\n * bytes. (A 0-field tuple is a valid prefix.)\n * @throws {ProtocolViolationError} on any violation.\n */\nfunction assertNamespaceTuple(fields: readonly Uint8Array[]): void {\n for (let i = 0; i < fields.length; i++) {\n if (!(fields[i] instanceof Uint8Array)) {\n throw new ProtocolViolationError(`Track Namespace field ${i} is not a Uint8Array`);\n }\n }\n validateTrackNamespacePrefix(fields as Uint8Array[]); // ≤32 fields, non-empty, ≤4096 bytes\n}\n\nfunction namespaceTupleLength(fields: readonly Uint8Array[]): number {\n assertNamespaceTuple(fields);\n let len = vi64EncodingLength(BigInt(fields.length));\n for (const f of fields) len += vi64EncodingLength(BigInt(f.length)) + f.length;\n return len;\n}\n\nfunction writeNamespaceTuple(fields: readonly Uint8Array[], buf: Uint8Array, offset: number): number {\n let p = offset + writeVi64(BigInt(fields.length), buf, offset);\n for (const f of fields) {\n p += writeVi64(BigInt(f.length), buf, p);\n buf.set(f, p);\n p += f.length;\n }\n return p - offset;\n}\n\nfunction readNamespaceTuple(buf: Uint8Array, offset: number): { value: Uint8Array[]; bytesRead: number } {\n const count = readVi64(buf, offset);\n let p = offset + count.bytesRead;\n const fields: Uint8Array[] = [];\n for (let i = 0n; i < count.value; i++) {\n const len = readVi64(buf, p);\n p += len.bytesRead;\n const n = Number(len.value);\n if (p + n > buf.length) {\n throw new ProtocolViolationError('Track Namespace field exceeds buffer');\n }\n fields.push(buf.slice(p, p + n));\n p += n;\n }\n // Reject malformed wire structure (>32 fields, empty field, >4096 bytes) at the\n // namespace-kind boundary rather than deferring to the session.\n validateTrackNamespacePrefix(fields);\n return { value: fields, bytesRead: p - offset };\n}\n\n/**\n * Message parameters keyed by Type. Values are arrays to allow parameter Types\n * that permit multiple instances (mirrors the draft-16 representation). Type is a\n * bigint to preserve the full vi64 range without precision loss.\n */\nexport type MessageParams18 = ReadonlyMap<bigint, readonly MessageParamValue[]>;\n\n/** Registry mapping a parameter Type to the encoding kind of its value. */\nexport type MessageParamRegistry = ReadonlyMap<bigint, ParamValueKind>;\n\n/** Default registry built from the draft-18 message-parameter type table (§10.2). */\nexport const DEFAULT_MESSAGE_PARAM_REGISTRY: MessageParamRegistry = new Map(\n Object.values(MessageParam18).map((d) => [BigInt(d.type), d.kind] as const),\n);\n\n/** Registry built from the draft-22 message-parameter type table. */\nexport const DRAFT22_MESSAGE_PARAM_REGISTRY: MessageParamRegistry = new Map(\n Object.values(MessageParam22).map((d) => [BigInt(d.type), d.kind] as const),\n);\n\n// ─── value length / write / read ─────────────────────────────────────\n\nfunction paramValueLength(v: MessageParamValue): number {\n switch (v.kind) {\n case 'uint8':\n return 1;\n case 'varint':\n return vi64EncodingLength(v.value);\n case 'location':\n return vi64EncodingLength(v.group) + vi64EncodingLength(v.object);\n case 'bytes':\n return vi64EncodingLength(BigInt(v.value.length)) + v.value.length;\n case 'namespace':\n return namespaceTupleLength(v.value);\n case 'locationFilter':\n return v.value.length;\n }\n}\n\nfunction writeParamValue(v: MessageParamValue, buf: Uint8Array, offset: number): number {\n switch (v.kind) {\n case 'uint8':\n return writeUint8(v.value, buf, offset);\n case 'varint':\n return writeVi64(v.value, buf, offset);\n case 'location': {\n let p = offset;\n p += writeVi64(v.group, buf, p);\n p += writeVi64(v.object, buf, p);\n return p - offset;\n }\n case 'bytes': {\n let p = offset;\n p += writeVi64(BigInt(v.value.length), buf, p);\n buf.set(v.value, p);\n p += v.value.length;\n return p - offset;\n }\n case 'namespace':\n return writeNamespaceTuple(v.value, buf, offset);\n case 'locationFilter':\n buf.set(v.value, offset);\n return v.value.length;\n }\n}\n\nfunction readParamValue(\n kind: ParamValueKind,\n buf: Uint8Array,\n offset: number,\n): { value: MessageParamValue; bytesRead: number } {\n switch (kind) {\n case 'uint8': {\n const { value, bytesRead } = readUint8(buf, offset);\n return { value: { kind: 'uint8', value }, bytesRead };\n }\n case 'varint': {\n const { value, bytesRead } = readVi64(buf, offset);\n return { value: { kind: 'varint', value }, bytesRead };\n }\n case 'location': {\n const g = readVi64(buf, offset);\n const o = readVi64(buf, offset + g.bytesRead);\n return {\n value: { kind: 'location', group: g.value, object: o.value },\n bytesRead: g.bytesRead + o.bytesRead,\n };\n }\n case 'bytes': {\n const len = readVi64(buf, offset);\n const start = offset + len.bytesRead;\n const n = Number(len.value);\n if (start + n > buf.length) {\n throw new ProtocolViolationError('Message parameter bytes value exceeds buffer');\n }\n return {\n value: { kind: 'bytes', value: buf.slice(start, start + n) },\n bytesRead: len.bytesRead + n,\n };\n }\n case 'namespace': {\n const { value, bytesRead } = readNamespaceTuple(buf, offset);\n return { value: { kind: 'namespace', value }, bytesRead };\n }\n case 'locationFilter': {\n const n = typedLocationFilterLength(buf, offset);\n return { value: { kind: 'locationFilter', value: buf.slice(offset, offset + n) }, bytesRead: n };\n }\n }\n}\n\n// ─── encode-side registry validation ────────────────────────────────\n\n/**\n * Enforce that every parameter's Type is known to `registry` and that its value\n * uses that Type's registered wire kind. The generic codec owns this \"known Type\n * uses the right wire kind\" rule; duplicate-parameter and message-scope checks\n * belong to the message-specific layer above.\n *\n * @throws {ProtocolViolationError} on an unknown Type or a kind mismatch.\n */\nfunction assertParamsMatchRegistry(params: MessageParams18, registry: MessageParamRegistry): void {\n for (const [type, values] of params) {\n const kind = registry.get(type);\n if (kind === undefined) {\n throw new ProtocolViolationError(`Cannot encode unknown message parameter Type 0x${type.toString(16)}`);\n }\n for (const value of values) {\n if (value.kind !== kind) {\n throw new ProtocolViolationError(\n `Message parameter Type 0x${type.toString(16)} expects ${kind}, got ${value.kind}`,\n );\n }\n }\n }\n}\n\n// ─── flatten (ascending type, delta-encoded) ─────────────────────────\n\ninterface FlatParam {\n readonly delta: bigint;\n readonly value: MessageParamValue;\n}\n\nfunction flatten(params: MessageParams18): FlatParam[] {\n const entries = [...params.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n const out: FlatParam[] = [];\n let prev = 0n;\n for (const [type, values] of entries) {\n for (const value of values) {\n out.push({ delta: type - prev, value });\n prev = type;\n }\n }\n return out;\n}\n\n// ─── public codec ────────────────────────────────────────────────────\n\n/** Wire length of the encoded parameter block (including the count prefix). */\nexport function messageParams18EncodingLength(\n params: MessageParams18,\n registry: MessageParamRegistry = DEFAULT_MESSAGE_PARAM_REGISTRY,\n): number {\n assertParamsMatchRegistry(params, registry);\n const flat = flatten(params);\n let len = vi64EncodingLength(BigInt(flat.length));\n for (const { delta, value } of flat) {\n len += vi64EncodingLength(delta) + paramValueLength(value);\n }\n return len;\n}\n\n/**\n * Encode message parameters to their draft-18 wire form.\n * @param registry Type→kind registry used to validate that each known Type uses\n * its correct wire kind (defaults to the standard draft-18 parameter table).\n * @throws {ProtocolViolationError} on an unknown Type or a value/kind mismatch.\n */\nexport function encodeMessageParams18(\n params: MessageParams18,\n registry: MessageParamRegistry = DEFAULT_MESSAGE_PARAM_REGISTRY,\n): Uint8Array {\n assertParamsMatchRegistry(params, registry);\n const flat = flatten(params);\n const buf = new Uint8Array(\n vi64EncodingLength(BigInt(flat.length)) +\n flat.reduce((n, { delta, value }) => n + vi64EncodingLength(delta) + paramValueLength(value), 0),\n );\n let p = writeVi64(BigInt(flat.length), buf, 0);\n for (const { delta, value } of flat) {\n p += writeVi64(delta, buf, p);\n p += writeParamValue(value, buf, p);\n }\n return buf;\n}\n\n/**\n * Decode message parameters from `buf` at `offset`, using `registry` to learn\n * each Type's value encoding.\n * @throws {ProtocolViolationError} on an unknown Type or a Type exceeding 2^64-1.\n */\nexport function decodeMessageParams18(\n buf: Uint8Array,\n offset: number,\n registry: MessageParamRegistry,\n): { params: MessageParams18; bytesRead: number } {\n const count = readVi64(buf, offset);\n const decoded = readParameterSequence(buf, offset + count.bytesRead, registry, count.value);\n return { params: decoded.params, bytesRead: count.bytesRead + decoded.bytesRead };\n}\n\n/** Decode a length-bounded parameter sequence without a count (draft-22 fills). */\nexport function decodeParameterSequence(buf: Uint8Array, registry: MessageParamRegistry): MessageParams18 {\n return readParameterSequence(buf, 0, registry).params;\n}\n\nfunction readParameterSequence(\n buf: Uint8Array,\n offset: number,\n registry: MessageParamRegistry,\n count?: bigint,\n): { params: MessageParams18; bytesRead: number } {\n let p = offset;\n\n const params = new Map<bigint, MessageParamValue[]>();\n let prevType = 0n;\n for (let i = 0n; count === undefined ? p < buf.length : i < count; i++) {\n const delta = readVi64(buf, p);\n p += delta.bytesRead;\n const type = prevType + delta.value;\n if (type > MAX_VI64) {\n throw new ProtocolViolationError(`Message parameter Type ${type} exceeds 2^64-1`);\n }\n prevType = type;\n\n const kind = registry.get(type);\n if (kind === undefined) {\n throw new ProtocolViolationError(`Unknown message parameter Type 0x${type.toString(16)}`);\n }\n const { value, bytesRead } = readParamValue(kind, buf, p);\n p += bytesRead;\n\n const existing = params.get(type);\n if (existing) existing.push(value);\n else params.set(type, [value]);\n }\n\n return { params, bytesRead: p - offset };\n}\n","/**\n * Reason Phrase encoding per draft-ietf-moq-transport-16 §1.4.3.\n * Varint length + UTF-8 bytes.\n * @module\n */\n\nimport { readVarint, writeVarint, varint, varintEncodingLength } from './varint.js';\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/** Read a Reason Phrase (varint length + UTF-8 string). */\nexport function readReasonPhrase(\n buf: Uint8Array,\n offset: number,\n): { value: string; bytesRead: number } {\n let pos = offset;\n const { value: length, bytesRead: lenBytes } = readVarint(buf, pos);\n pos += lenBytes;\n const numLen = Number(length);\n if (numLen > 1024) {\n throw new RangeError(\n `Reason Phrase length ${numLen} exceeds maximum 1024 bytes`,\n );\n }\n if (pos + numLen > buf.length) {\n throw new RangeError(\n `Reason Phrase declared length ${numLen} exceeds remaining buffer (${buf.length - pos} bytes available)`,\n );\n }\n const strBytes = buf.slice(pos, pos + numLen);\n const value = decoder.decode(strBytes);\n pos += numLen;\n return { value, bytesRead: pos - offset };\n}\n\n/** Write a Reason Phrase (varint length + UTF-8 string). Returns bytes written. */\nexport function writeReasonPhrase(\n reason: string,\n buf: Uint8Array,\n offset: number,\n): number {\n const encoded = encoder.encode(reason);\n if (encoded.length > 1024) {\n throw new RangeError(\n `Reason Phrase length ${encoded.length} exceeds maximum 1024 bytes`,\n );\n }\n let pos = offset;\n pos += writeVarint(varint(encoded.length), buf, pos);\n buf.set(encoded, pos);\n pos += encoded.length;\n return pos - offset;\n}\n\n/** Calculate encoding length for a Reason Phrase. */\nexport function reasonPhraseEncodingLength(reason: string): number {\n const byteLen = encoder.encode(reason).length;\n return varintEncodingLength(varint(byteLen)) + byteLen;\n}\n","/**\n * Control message type codes.\n * @see draft-ietf-moq-transport-16 §9\n * @module\n */\n\nimport { varint } from '../primitives/varint.js';\n\n/**\n * Wire type codes for all MOQT control messages.\n * @see draft-ietf-moq-transport-16 §9\n */\nexport const MessageType = {\n REQUEST_UPDATE: varint(0x02),\n SUBSCRIBE: varint(0x03),\n SUBSCRIBE_OK: varint(0x04),\n REQUEST_ERROR: varint(0x05),\n PUBLISH_NAMESPACE: varint(0x06),\n REQUEST_OK: varint(0x07),\n NAMESPACE: varint(0x08),\n PUBLISH_NAMESPACE_DONE: varint(0x09),\n UNSUBSCRIBE: varint(0x0a),\n PUBLISH_DONE: varint(0x0b),\n PUBLISH_NAMESPACE_CANCEL: varint(0x0c),\n TRACK_STATUS: varint(0x0d),\n NAMESPACE_DONE: varint(0x0e),\n GOAWAY: varint(0x10),\n SUBSCRIBE_NAMESPACE: varint(0x11),\n MAX_REQUEST_ID: varint(0x15),\n FETCH: varint(0x16),\n FETCH_CANCEL: varint(0x17),\n FETCH_OK: varint(0x18),\n REQUESTS_BLOCKED: varint(0x1a),\n PUBLISH: varint(0x1d),\n PUBLISH_OK: varint(0x1e),\n CLIENT_SETUP: varint(0x20),\n SERVER_SETUP: varint(0x21),\n} as const;\n\nexport type MessageTypeCode = (typeof MessageType)[keyof typeof MessageType];\n","/**\n * Setup and message parameter type codes.\n * @see draft-ietf-moq-transport-16 §9.2.2, §9.3.1\n * @module\n */\n\nimport { varint } from '../primitives/varint.js';\n\n/**\n * Setup parameter type codes (version-independent).\n * @see draft-ietf-moq-transport-16 §9.3.1\n */\nexport const SetupParam = {\n /** @see §9.3.1.2 */\n PATH: varint(0x01),\n /** @see §9.3.1.3 */\n MAX_REQUEST_ID: varint(0x02),\n /** @see §9.3.1.5 */\n AUTHORIZATION_TOKEN: varint(0x03),\n /** @see §9.3.1.4 */\n MAX_AUTH_TOKEN_CACHE_SIZE: varint(0x04),\n /** @see §9.3.1.1 */\n AUTHORITY: varint(0x05),\n /** @see §9.3.1.6 */\n MOQT_IMPLEMENTATION: varint(0x07),\n} as const;\n\n/**\n * Message parameter type codes (version-specific).\n * @see draft-ietf-moq-transport-16 §9.2.2\n */\nexport const MessageParam = {\n /** @see §9.2.2.2 */\n DELIVERY_TIMEOUT: varint(0x02),\n /**\n * draft-18 §10.2.4: OBJECT_DELIVERY_TIMEOUT — same wire type (0x02) as the\n * draft-14/16 DELIVERY_TIMEOUT; named alias for the draft-18 scope tables.\n */\n OBJECT_DELIVERY_TIMEOUT: varint(0x02),\n /** @see §9.2.2.1 */\n AUTHORIZATION_TOKEN: varint(0x03),\n /** draft-18 §10.2.6: RENDEZVOUS_TIMEOUT — SUBSCRIBE only. */\n RENDEZVOUS_TIMEOUT: varint(0x04),\n /** draft-18 §10.2.3: SUBGROUP_DELIVERY_TIMEOUT — PUBLISH_OK / SUBSCRIBE / REQUEST_UPDATE. */\n SUBGROUP_DELIVERY_TIMEOUT: varint(0x06),\n /** @see §9.2.2.6 */\n EXPIRES: varint(0x08),\n /** @see §9.2.2.7 */\n LARGEST_OBJECT: varint(0x09),\n /** draft-18 §10.2.5: FILL_TIMEOUT — FETCH only. */\n FILL_TIMEOUT: varint(0x0a),\n /** @see §9.2.2.8 */\n FORWARD: varint(0x10),\n /** @see §9.2.2.3 */\n SUBSCRIBER_PRIORITY: varint(0x20),\n /** @see §9.2.2.5 */\n SUBSCRIPTION_FILTER: varint(0x21),\n /** @see §9.2.2.4 */\n GROUP_ORDER: varint(0x22),\n /** @see §9.2.2.9 */\n NEW_GROUP_REQUEST: varint(0x32),\n /**\n * draft-18 §10.2.14: Track Namespace Prefix. Carried in REQUEST_UPDATE to\n * change the prefix of a SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS. Its value is a\n * Track Namespace tuple (semantic `Uint8Array[]`), not a KVP scalar — the\n * draft-14/16 codecs reject it at the KVP boundary.\n */\n TRACK_NAMESPACE_PREFIX: varint(0x34),\n} as const;\n","/**\n * Control message encoder.\n * Serializes ControlMessage → Uint8Array with framing.\n *\n * Wire format: Type (varint) + Length (uint16) + Payload\n *\n * @see draft-ietf-moq-transport-16 §9\n * @module\n */\n\nimport type { ControlMessage, Parameters, TrackExtensions } from './messages.js';\nimport { MessageType } from './codes.js';\nimport { varint, writeVarint, varintEncodingLength, type Varint } from '../primitives/varint.js';\nimport {\n writeLengthPrefixedBytes,\n lengthPrefixedBytesEncodingLength,\n writeTuple,\n tupleEncodingLength,\n validateTrackNamespace,\n validateTrackNamespacePrefix,\n validateTrackNamespaceSuffix,\n validateFullTrackName,\n} from '../primitives/bytes.js';\nimport { writeKvpList, kvpListEncodingLength, kvpListEntryCount, type KvpValue } from '../primitives/kvp.js';\nimport { toKvpParams } from './kvp-params.js';\nimport { MessageParam } from './parameters.js';\nimport { ProtocolViolationError } from '../errors.js';\nimport { writeLocation, locationEncodingLength } from '../primitives/location.js';\nimport { writeReasonPhrase, reasonPhraseEncodingLength } from '../primitives/reason.js';\n\n/**\n * Known message parameter types that must be unique (all except AUTHORIZATION_TOKEN).\n * @see draft-ietf-moq-transport-16 §9.2.2\n */\nconst UNIQUE_MESSAGE_PARAMS = new Set<bigint>([\n MessageParam.DELIVERY_TIMEOUT as bigint,\n MessageParam.EXPIRES as bigint,\n MessageParam.LARGEST_OBJECT as bigint,\n MessageParam.FORWARD as bigint,\n MessageParam.SUBSCRIBER_PRIORITY as bigint,\n MessageParam.SUBSCRIPTION_FILTER as bigint,\n MessageParam.GROUP_ORDER as bigint,\n MessageParam.NEW_GROUP_REQUEST as bigint,\n]);\n\n/** Human-readable names for message parameters (for error messages). */\nconst MESSAGE_PARAM_NAMES = new Map<bigint, string>([\n [MessageParam.DELIVERY_TIMEOUT as bigint, 'DELIVERY_TIMEOUT'],\n [MessageParam.EXPIRES as bigint, 'EXPIRES'],\n [MessageParam.LARGEST_OBJECT as bigint, 'LARGEST_OBJECT'],\n [MessageParam.FORWARD as bigint, 'FORWARD'],\n [MessageParam.SUBSCRIBER_PRIORITY as bigint, 'SUBSCRIBER_PRIORITY'],\n [MessageParam.SUBSCRIPTION_FILTER as bigint, 'SUBSCRIPTION_FILTER'],\n [MessageParam.GROUP_ORDER as bigint, 'GROUP_ORDER'],\n [MessageParam.NEW_GROUP_REQUEST as bigint, 'NEW_GROUP_REQUEST'],\n]);\n\n/**\n * Validate message parameters for sender-side duplicate constraints.\n * §9.2.2.1: AUTHORIZATION_TOKEN may appear multiple times.\n * All other known message parameters must be unique.\n * @throws {Error} If a parameter that must be unique appears multiple times\n */\nfunction validateMessageParams(params: Parameters): void {\n for (const [key, values] of params) {\n if (UNIQUE_MESSAGE_PARAMS.has(key as bigint) && values.length > 1) {\n const name = MESSAGE_PARAM_NAMES.get(key as bigint) ?? `0x${(key as bigint).toString(16)}`;\n throw new Error(`Duplicate message parameter: ${name} may not appear multiple times`);\n }\n if (key === MessageParam.FORWARD) {\n const value = values[0];\n if (values.length !== 1 || typeof value !== 'bigint'\n || (value !== 0n && value !== 1n)) {\n throw new ProtocolViolationError('FORWARD must be exactly one value, 0 or 1');\n }\n }\n }\n}\n\n/**\n * Check if a message has message parameters (vs setup parameters).\n * Setup params are validated in the session layer with different rules.\n */\nfunction hasMessageParams(msg: ControlMessage): msg is ControlMessage & { parameters: Parameters } {\n return msg.type !== 'CLIENT_SETUP' && msg.type !== 'SERVER_SETUP' && 'parameters' in msg;\n}\n\n/**\n * Validate namespace fields in a message before encoding.\n * @throws {Error} If any namespace constraint is violated per §2.4.1\n */\nfunction validateMessageNamespaces(msg: ControlMessage): void {\n switch (msg.type) {\n case 'SUBSCRIBE':\n case 'TRACK_STATUS':\n case 'PUBLISH':\n validateFullTrackName(msg.trackNamespace, msg.trackName);\n break;\n\n case 'FETCH':\n if (msg.fetch.fetchType === 0x1) {\n validateFullTrackName(msg.fetch.trackNamespace, msg.fetch.trackName);\n }\n break;\n\n case 'PUBLISH_NAMESPACE':\n validateTrackNamespace(msg.trackNamespace);\n break;\n\n case 'NAMESPACE':\n case 'NAMESPACE_DONE':\n // Validate suffix: ≤32 fields, each ≥1 byte, total ≤4096 bytes\n validateTrackNamespaceSuffix(msg.trackNamespaceSuffix);\n break;\n\n case 'SUBSCRIBE_NAMESPACE':\n validateTrackNamespacePrefix(msg.trackNamespacePrefix);\n break;\n\n // Messages without namespaces - no validation needed\n default:\n break;\n }\n}\n\n/**\n * Encode a control message to its wire representation.\n * Returns a complete framed message (type + length + payload).\n */\nexport function encodeControlMessage(msg: ControlMessage): Uint8Array {\n // Validate namespace constraints before encoding\n validateMessageNamespaces(msg);\n\n // Validate message params for duplicates (setup params handled by session layer)\n if (hasMessageParams(msg)) {\n validateMessageParams(msg.parameters);\n }\n\n const typeCode = messageTypeCode(msg.type);\n const payloadLen = payloadLength(msg);\n if (payloadLen > 0xffff) {\n throw new RangeError(\n `Control message payload length ${payloadLen} exceeds maximum 65535 (uint16)`,\n );\n }\n const typeLen = varintEncodingLength(typeCode);\n const totalLen = typeLen + 2 + payloadLen; // type + uint16 length + payload\n\n const buf = new Uint8Array(totalLen);\n let pos = 0;\n\n // Type (varint)\n pos += writeVarint(typeCode, buf, pos);\n\n // Length (uint16 big-endian)\n buf[pos++] = (payloadLen >> 8) & 0xff;\n buf[pos++] = payloadLen & 0xff;\n\n // Payload\n pos += writePayload(msg, buf, pos);\n\n return buf;\n}\n\nfunction messageTypeCode(type: ControlMessage['type']): Varint {\n switch (type) {\n case 'CLIENT_SETUP': return MessageType.CLIENT_SETUP;\n case 'SERVER_SETUP': return MessageType.SERVER_SETUP;\n case 'SETUP':\n throw new Error('SETUP is a draft-18 message; the draft-14/16 codec uses CLIENT_SETUP/SERVER_SETUP');\n case 'GOAWAY': return MessageType.GOAWAY;\n case 'MAX_REQUEST_ID': return MessageType.MAX_REQUEST_ID;\n case 'REQUESTS_BLOCKED': return MessageType.REQUESTS_BLOCKED;\n case 'REQUEST_OK': return MessageType.REQUEST_OK;\n case 'REQUEST_ERROR': return MessageType.REQUEST_ERROR;\n case 'SUBSCRIBE': return MessageType.SUBSCRIBE;\n case 'SUBSCRIBE_OK': return MessageType.SUBSCRIBE_OK;\n case 'REQUEST_UPDATE': return MessageType.REQUEST_UPDATE;\n case 'UNSUBSCRIBE': return MessageType.UNSUBSCRIBE;\n case 'PUBLISH': return MessageType.PUBLISH;\n case 'PUBLISH_OK': return MessageType.PUBLISH_OK;\n case 'PUBLISH_DONE': return MessageType.PUBLISH_DONE;\n case 'FETCH': return MessageType.FETCH;\n case 'FETCH_OK': return MessageType.FETCH_OK;\n case 'FETCH_CANCEL': return MessageType.FETCH_CANCEL;\n case 'TRACK_STATUS': return MessageType.TRACK_STATUS;\n case 'PUBLISH_NAMESPACE': return MessageType.PUBLISH_NAMESPACE;\n case 'NAMESPACE': return MessageType.NAMESPACE;\n case 'PUBLISH_NAMESPACE_DONE': return MessageType.PUBLISH_NAMESPACE_DONE;\n case 'NAMESPACE_DONE': return MessageType.NAMESPACE_DONE;\n case 'PUBLISH_NAMESPACE_CANCEL': return MessageType.PUBLISH_NAMESPACE_CANCEL;\n case 'SUBSCRIBE_NAMESPACE': return MessageType.SUBSCRIBE_NAMESPACE;\n // Draft-14-only types — not valid in draft-16 encoder\n case 'UNSUBSCRIBE_NAMESPACE':\n case 'PUBLISH_NAMESPACE_OK':\n case 'PUBLISH_NAMESPACE_ERROR':\n case 'PUBLISH_ERROR':\n throw new Error(`Cannot encode draft-14-only message type \"${type}\" with draft-16 encoder`);\n // Draft-18-only types — not valid in the draft-14/16 encoder\n case 'SUBSCRIBE_TRACKS':\n case 'PUBLISH_BLOCKED':\n case 'PUBLISH_STATE_NOTIFY':\n throw new Error(`Cannot encode draft-18-only message type \"${type}\" with draft-16 encoder`);\n }\n}\n\n/** Calculate parameters encoding length: varint(count) + KVP data. */\nfunction paramsLength(params: Parameters): number {\n const kvp = toKvpParams(params);\n const count = kvpListEntryCount(kvp);\n return varintEncodingLength(varint(count)) + kvpListEncodingLength(kvp);\n}\n\n/** Write parameters: varint(count) + KVP data. Returns bytes written. */\nfunction writeParams(params: Parameters, buf: Uint8Array, offset: number): number {\n let pos = offset;\n const kvp = toKvpParams(params);\n const count = kvpListEntryCount(kvp);\n pos += writeVarint(varint(count), buf, pos);\n pos += writeKvpList(kvp, buf, pos);\n return pos - offset;\n}\n\n// draft-14/16 Track Extensions are QUIC-range KVP; the shared TrackProperties type\n// is now vi64-wide (full uint64), so cast at this draft-16 boundary — the KVP\n// writer still range-validates each value via writeVarint().\nconst asKvp = (ext: TrackExtensions): Map<bigint, KvpValue[]> => ext as unknown as Map<bigint, KvpValue[]>;\n\n/** Calculate track extensions encoding length. Extensions consume remaining bytes, no count prefix. */\nfunction extensionsLength(ext: TrackExtensions): number {\n if (ext.size === 0) return 0;\n // Extensions are written as KVP entries without a count prefix\n return kvpListEncodingLength(asKvp(ext));\n}\n\n/** Write track extensions (no count prefix). */\nfunction writeExtensions(ext: TrackExtensions, buf: Uint8Array, offset: number): number {\n if (ext.size === 0) return 0;\n return writeKvpList(asKvp(ext), buf, offset);\n}\n\n/** Track name: varint length + bytes. */\nfunction trackNameLength(name: Uint8Array): number {\n return lengthPrefixedBytesEncodingLength(name);\n}\n\nfunction writeTrackName(name: Uint8Array, buf: Uint8Array, offset: number): number {\n return writeLengthPrefixedBytes(name, buf, offset);\n}\n\nfunction payloadLength(msg: ControlMessage): number {\n switch (msg.type) {\n case 'CLIENT_SETUP':\n case 'SERVER_SETUP':\n return paramsLength(msg.parameters);\n\n case 'SETUP':\n throw new Error('SETUP is a draft-18 message; not supported by the draft-14/16 codec');\n\n case 'GOAWAY': {\n const encoded = new TextEncoder().encode(msg.newSessionUri);\n if (encoded.length > 8192) {\n throw new RangeError(\n `GOAWAY URI length ${encoded.length} exceeds maximum 8192 bytes`,\n );\n }\n return varintEncodingLength(varint(encoded.length)) + encoded.length;\n }\n\n case 'MAX_REQUEST_ID':\n return varintEncodingLength(msg.maxRequestId);\n\n case 'REQUESTS_BLOCKED':\n return varintEncodingLength(msg.maximumRequestId);\n\n case 'REQUEST_OK':\n return varintEncodingLength(msg.requestId) + paramsLength(msg.parameters);\n\n case 'REQUEST_ERROR':\n return (\n varintEncodingLength(msg.requestId) +\n varintEncodingLength(msg.errorCode) +\n varintEncodingLength(msg.retryInterval) +\n reasonPhraseEncodingLength(msg.errorReason)\n );\n\n case 'SUBSCRIBE':\n case 'TRACK_STATUS':\n return (\n varintEncodingLength(msg.requestId) +\n tupleEncodingLength(msg.trackNamespace) +\n trackNameLength(msg.trackName) +\n paramsLength(msg.parameters)\n );\n\n case 'SUBSCRIBE_OK':\n return (\n varintEncodingLength(msg.requestId) +\n varintEncodingLength(msg.trackAlias) +\n paramsLength(msg.parameters) +\n extensionsLength(msg.trackProperties ?? msg.trackExtensions ?? new Map())\n );\n\n case 'REQUEST_UPDATE':\n return (\n varintEncodingLength(msg.requestId) +\n varintEncodingLength(msg.existingRequestId) +\n paramsLength(msg.parameters)\n );\n\n case 'UNSUBSCRIBE':\n return varintEncodingLength(msg.requestId);\n\n case 'PUBLISH':\n return (\n varintEncodingLength(msg.requestId) +\n tupleEncodingLength(msg.trackNamespace) +\n trackNameLength(msg.trackName) +\n varintEncodingLength(msg.trackAlias) +\n paramsLength(msg.parameters) +\n extensionsLength(msg.trackProperties ?? msg.trackExtensions ?? new Map())\n );\n\n case 'PUBLISH_OK':\n return (\n varintEncodingLength(msg.requestId) +\n paramsLength(msg.parameters)\n );\n\n case 'PUBLISH_DONE':\n return (\n varintEncodingLength(msg.requestId) +\n varintEncodingLength(msg.statusCode) +\n varintEncodingLength(msg.streamCount) +\n reasonPhraseEncodingLength(msg.errorReason)\n );\n\n case 'FETCH': {\n let len = varintEncodingLength(msg.requestId) + varintEncodingLength(varint(msg.fetch.fetchType));\n if (msg.fetch.fetchType === 0x1) {\n len +=\n tupleEncodingLength(msg.fetch.trackNamespace) +\n trackNameLength(msg.fetch.trackName) +\n locationEncodingLength(msg.fetch.startLocation) +\n locationEncodingLength(msg.fetch.endLocation);\n } else {\n len +=\n varintEncodingLength(msg.fetch.joiningRequestId) +\n varintEncodingLength(msg.fetch.joiningStart);\n }\n len += paramsLength(msg.parameters);\n return len;\n }\n\n case 'FETCH_OK':\n return (\n varintEncodingLength(msg.requestId) +\n 1 + // End Of Track (uint8)\n locationEncodingLength(msg.endLocation) +\n paramsLength(msg.parameters) +\n extensionsLength(msg.trackProperties ?? msg.trackExtensions ?? new Map())\n );\n\n case 'FETCH_CANCEL':\n return varintEncodingLength(msg.requestId);\n\n case 'PUBLISH_NAMESPACE':\n return (\n varintEncodingLength(msg.requestId) +\n tupleEncodingLength(msg.trackNamespace) +\n paramsLength(msg.parameters)\n );\n\n case 'NAMESPACE':\n return tupleEncodingLength(msg.trackNamespaceSuffix);\n\n case 'PUBLISH_NAMESPACE_DONE':\n return varintEncodingLength(msg.requestId!);\n\n case 'NAMESPACE_DONE':\n return tupleEncodingLength(msg.trackNamespaceSuffix);\n\n case 'PUBLISH_NAMESPACE_CANCEL':\n return (\n varintEncodingLength(msg.requestId!) +\n varintEncodingLength(msg.errorCode) +\n reasonPhraseEncodingLength(msg.errorReason)\n );\n\n case 'SUBSCRIBE_NAMESPACE':\n return (\n varintEncodingLength(msg.requestId) +\n tupleEncodingLength(msg.trackNamespacePrefix) +\n varintEncodingLength(msg.subscribeOptions!) +\n paramsLength(msg.parameters)\n );\n\n // Draft-14-only types — unreachable (messageTypeCode throws first)\n case 'UNSUBSCRIBE_NAMESPACE':\n case 'PUBLISH_NAMESPACE_OK':\n case 'PUBLISH_NAMESPACE_ERROR':\n case 'PUBLISH_ERROR':\n throw new Error(`Cannot encode draft-14-only message type \"${msg.type}\" with draft-16 encoder`);\n // Draft-18-only types — unreachable (messageTypeCode throws first)\n case 'SUBSCRIBE_TRACKS':\n case 'PUBLISH_BLOCKED':\n case 'PUBLISH_STATE_NOTIFY':\n throw new Error(`Cannot encode draft-18-only message type \"${msg.type}\" with draft-16 encoder`);\n }\n}\n\nfunction writePayload(msg: ControlMessage, buf: Uint8Array, offset: number): number {\n let pos = offset;\n\n switch (msg.type) {\n case 'CLIENT_SETUP':\n case 'SERVER_SETUP':\n pos += writeParams(msg.parameters, buf, pos);\n break;\n\n case 'GOAWAY': {\n const encoded = new TextEncoder().encode(msg.newSessionUri);\n pos += writeVarint(varint(encoded.length), buf, pos);\n buf.set(encoded, pos);\n pos += encoded.length;\n break;\n }\n\n case 'MAX_REQUEST_ID':\n pos += writeVarint(msg.maxRequestId, buf, pos);\n break;\n\n case 'REQUESTS_BLOCKED':\n pos += writeVarint(msg.maximumRequestId, buf, pos);\n break;\n\n case 'REQUEST_OK':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeParams(msg.parameters, buf, pos);\n break;\n\n case 'REQUEST_ERROR':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeVarint(msg.errorCode, buf, pos);\n pos += writeVarint(msg.retryInterval, buf, pos);\n pos += writeReasonPhrase(msg.errorReason, buf, pos);\n break;\n\n case 'SUBSCRIBE':\n case 'TRACK_STATUS':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeTuple(msg.trackNamespace, buf, pos);\n pos += writeTrackName(msg.trackName, buf, pos);\n pos += writeParams(msg.parameters, buf, pos);\n break;\n\n case 'SUBSCRIBE_OK':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeVarint(msg.trackAlias, buf, pos);\n pos += writeParams(msg.parameters, buf, pos);\n pos += writeExtensions(msg.trackProperties ?? msg.trackExtensions ?? new Map(), buf, pos);\n break;\n\n case 'REQUEST_UPDATE':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeVarint(msg.existingRequestId, buf, pos);\n pos += writeParams(msg.parameters, buf, pos);\n break;\n\n case 'UNSUBSCRIBE':\n pos += writeVarint(msg.requestId, buf, pos);\n break;\n\n case 'PUBLISH':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeTuple(msg.trackNamespace, buf, pos);\n pos += writeTrackName(msg.trackName, buf, pos);\n pos += writeVarint(msg.trackAlias, buf, pos);\n pos += writeParams(msg.parameters, buf, pos);\n pos += writeExtensions(msg.trackProperties ?? msg.trackExtensions ?? new Map(), buf, pos);\n break;\n\n case 'PUBLISH_OK':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeParams(msg.parameters, buf, pos);\n break;\n\n case 'PUBLISH_DONE':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeVarint(msg.statusCode, buf, pos);\n pos += writeVarint(msg.streamCount, buf, pos);\n pos += writeReasonPhrase(msg.errorReason, buf, pos);\n break;\n\n case 'FETCH':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeVarint(varint(msg.fetch.fetchType), buf, pos);\n if (msg.fetch.fetchType === 0x1) {\n pos += writeTuple(msg.fetch.trackNamespace, buf, pos);\n pos += writeTrackName(msg.fetch.trackName, buf, pos);\n pos += writeLocation(msg.fetch.startLocation, buf, pos);\n pos += writeLocation(msg.fetch.endLocation, buf, pos);\n } else {\n pos += writeVarint(msg.fetch.joiningRequestId, buf, pos);\n pos += writeVarint(msg.fetch.joiningStart, buf, pos);\n }\n pos += writeParams(msg.parameters, buf, pos);\n break;\n\n case 'FETCH_OK':\n pos += writeVarint(msg.requestId, buf, pos);\n buf[pos++] = msg.endOfTrack & 0xff;\n pos += writeLocation(msg.endLocation, buf, pos);\n pos += writeParams(msg.parameters, buf, pos);\n pos += writeExtensions(msg.trackProperties ?? msg.trackExtensions ?? new Map(), buf, pos);\n break;\n\n case 'FETCH_CANCEL':\n pos += writeVarint(msg.requestId, buf, pos);\n break;\n\n case 'PUBLISH_NAMESPACE':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeTuple(msg.trackNamespace, buf, pos);\n pos += writeParams(msg.parameters, buf, pos);\n break;\n\n case 'NAMESPACE':\n pos += writeTuple(msg.trackNamespaceSuffix, buf, pos);\n break;\n\n case 'PUBLISH_NAMESPACE_DONE':\n pos += writeVarint(msg.requestId!, buf, pos);\n break;\n\n case 'NAMESPACE_DONE':\n pos += writeTuple(msg.trackNamespaceSuffix, buf, pos);\n break;\n\n case 'PUBLISH_NAMESPACE_CANCEL':\n pos += writeVarint(msg.requestId!, buf, pos);\n pos += writeVarint(msg.errorCode, buf, pos);\n pos += writeReasonPhrase(msg.errorReason, buf, pos);\n break;\n\n case 'SUBSCRIBE_NAMESPACE':\n pos += writeVarint(msg.requestId, buf, pos);\n pos += writeTuple(msg.trackNamespacePrefix, buf, pos);\n pos += writeVarint(msg.subscribeOptions!, buf, pos);\n pos += writeParams(msg.parameters, buf, pos);\n break;\n\n // Draft-14-only types — unreachable (messageTypeCode throws first)\n case 'UNSUBSCRIBE_NAMESPACE':\n case 'PUBLISH_NAMESPACE_OK':\n case 'PUBLISH_NAMESPACE_ERROR':\n case 'PUBLISH_ERROR':\n throw new Error(`Cannot encode draft-14-only message type \"${msg.type}\" with draft-16 encoder`);\n // Draft-18-only types — unreachable (messageTypeCode throws first)\n case 'SUBSCRIBE_TRACKS':\n case 'PUBLISH_BLOCKED':\n case 'PUBLISH_STATE_NOTIFY':\n throw new Error(`Cannot encode draft-18-only message type \"${msg.type}\" with draft-16 encoder`);\n }\n\n return pos - offset;\n}\n","/**\n * Control message decoder.\n * Deserializes Uint8Array → ControlMessage.\n *\n * Wire format: Type (varint) + Length (uint16) + Payload\n *\n * @see draft-ietf-moq-transport-16 §9\n * @module\n */\n\nimport type {\n ControlMessage,\n Parameters,\n TrackExtensions,\n StandaloneFetch,\n JoiningFetch,\n} from './messages.js';\nimport { MessageType } from './codes.js';\nimport { readVarint, varint, type Varint } from '../primitives/varint.js';\nimport {\n readUint8,\n readLengthPrefixedBytes,\n readTuple,\n validateTrackNamespace,\n validateTrackNamespacePrefix,\n validateTrackNamespaceSuffix,\n validateFullTrackName,\n} from '../primitives/bytes.js';\nimport { readKvpList, type KvpValue } from '../primitives/kvp.js';\nimport { readLocation } from '../primitives/location.js';\nimport { readReasonPhrase } from '../primitives/reason.js';\nimport { ProtocolViolationError } from '../errors.js';\n\n/**\n * Decode a control message from its wire representation.\n * @param buf Buffer containing at least one complete framed message.\n * @param offset Starting position in the buffer (default 0).\n * @returns The decoded message and total bytes consumed (type + length + payload).\n */\nexport function decodeControlMessage(\n buf: Uint8Array,\n offset: number = 0,\n): { message: ControlMessage; bytesRead: number } {\n let pos = offset;\n\n // Read message type (varint)\n const { value: msgType, bytesRead: typeBytes } = readVarint(buf, pos);\n pos += typeBytes;\n\n // Read message length (uint16 big-endian)\n if (pos + 2 > buf.length) {\n throw new RangeError('Buffer too short for message length field');\n }\n const payloadLen = (buf[pos]! << 8) | buf[pos + 1]!;\n pos += 2;\n\n const payloadStart = pos;\n const payloadEnd = payloadStart + payloadLen;\n\n if (payloadEnd > buf.length) {\n throw new RangeError(\n `Buffer too short for message payload: need ${payloadLen} bytes, have ${buf.length - pos}`,\n );\n }\n\n const message = decodePayload(msgType, buf, pos, payloadEnd);\n const totalBytesRead = typeBytes + 2 + payloadLen;\n\n return { message, bytesRead: totalBytesRead };\n}\n\n/** Read parameters: varint(count) + KVP data. */\nfunction readParams(\n buf: Uint8Array,\n offset: number,\n): { value: Parameters; bytesRead: number } {\n let pos = offset;\n const { value: count, bytesRead: countBytes } = readVarint(buf, pos);\n pos += countBytes;\n const { value, bytesRead: kvpBytes } = readKvpList(buf, pos, Number(count));\n pos += kvpBytes;\n return { value, bytesRead: pos - offset };\n}\n\n/**\n * Read track extensions from remaining bytes.\n * Extensions are KVP entries with no count prefix — we read one at a time until end.\n */\nfunction readExtensions(\n buf: Uint8Array,\n offset: number,\n end: number,\n): { value: TrackExtensions; bytesRead: number } {\n const remaining = end - offset;\n if (remaining <= 0) {\n return { value: new Map<Varint, KvpValue[]>(), bytesRead: 0 };\n }\n // Count how many KVP entries fit by reading them\n const result = new Map<Varint, KvpValue[]>();\n let pos = offset;\n let prevType = 0n;\n\n while (pos < end) {\n const { value: delta, bytesRead: deltaBytes } = readVarint(buf, pos);\n pos += deltaBytes;\n const absType = prevType + delta;\n prevType = absType;\n const key = varint(absType);\n\n let val: KvpValue;\n if (absType % 2n === 0n) {\n const { value: v, bytesRead: valBytes } = readVarint(buf, pos);\n pos += valBytes;\n val = v;\n } else {\n const { value: v, bytesRead: valBytes } = readLengthPrefixedBytes(buf, pos);\n // §1.4.2: \"The maximum length of a value is 2^16-1 bytes\"\n if (v.length > 0xffff) {\n throw new RangeError(\n `Track extension byte value length ${v.length} exceeds maximum 65535 (2^16-1)`,\n );\n }\n pos += valBytes;\n val = v;\n }\n\n // Collect into array (supports duplicates)\n const existing = result.get(key);\n if (existing) {\n existing.push(val);\n } else {\n result.set(key, [val]);\n }\n }\n\n return { value: result, bytesRead: pos - offset };\n}\n\n/** Read Track Name (length-prefixed bytes). */\nfunction readTrackName(\n buf: Uint8Array,\n offset: number,\n): { value: Uint8Array; bytesRead: number } {\n return readLengthPrefixedBytes(buf, offset);\n}\n\n\n// Build a reverse lookup from code → type name\nconst codeToType = new Map<bigint, ControlMessage['type']>();\nfor (const [name, code] of Object.entries(MessageType)) {\n codeToType.set(code, name as ControlMessage['type']);\n}\n\nfunction decodePayload(\n msgType: Varint,\n buf: Uint8Array,\n offset: number,\n payloadEnd: number,\n): ControlMessage {\n const typeName = codeToType.get(msgType);\n\n if (typeName === undefined) {\n throw new ProtocolViolationError(`Unknown message type: 0x${msgType.toString(16)}`);\n }\n\n const { message, finalPos } = decodePayloadFields(typeName, buf, offset, payloadEnd);\n\n if (finalPos !== payloadEnd) {\n throw new RangeError(\n `Payload not fully consumed for ${typeName}: consumed ${finalPos - offset} bytes, expected ${payloadEnd - offset}`,\n );\n }\n\n return message;\n}\n\nfunction decodePayloadFields(\n typeName: ControlMessage['type'],\n buf: Uint8Array,\n offset: number,\n payloadEnd: number,\n): { message: ControlMessage; finalPos: number } {\n let pos = offset;\n\n switch (typeName) {\n case 'CLIENT_SETUP':\n case 'SERVER_SETUP': {\n const { value: parameters, bytesRead } = readParams(buf, pos);\n pos += bytesRead;\n return { message: { type: typeName, parameters }, finalPos: pos };\n }\n\n case 'SETUP':\n // draft-18-only; the draft-14/16 decoder never produces this typeName.\n throw new Error('SETUP is a draft-18 message; not produced by the draft-14/16 codec');\n\n case 'GOAWAY': {\n const { value: uriLen, bytesRead: uriLenBytes } = readVarint(buf, pos);\n pos += uriLenBytes;\n const numUriLen = Number(uriLen);\n if (numUriLen > 8192) {\n throw new RangeError(\n `GOAWAY URI length ${numUriLen} exceeds maximum 8192 bytes`,\n );\n }\n const uriBytes = buf.slice(pos, pos + numUriLen);\n pos += numUriLen;\n const newSessionUri = new TextDecoder().decode(uriBytes);\n return { message: { type: 'GOAWAY', newSessionUri }, finalPos: pos };\n }\n\n case 'MAX_REQUEST_ID': {\n const { value: maxRequestId, bytesRead } = readVarint(buf, pos);\n pos += bytesRead;\n return { message: { type: 'MAX_REQUEST_ID', maxRequestId }, finalPos: pos };\n }\n\n case 'REQUESTS_BLOCKED': {\n const { value: maximumRequestId, bytesRead } = readVarint(buf, pos);\n pos += bytesRead;\n return { message: { type: 'REQUESTS_BLOCKED', maximumRequestId }, finalPos: pos };\n }\n\n case 'REQUEST_OK': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: parameters, bytesRead: pBytes } = readParams(buf, pos);\n pos += pBytes;\n return { message: { type: 'REQUEST_OK', requestId, parameters }, finalPos: pos };\n }\n\n case 'REQUEST_ERROR': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: errorCode, bytesRead: ecBytes } = readVarint(buf, pos);\n pos += ecBytes;\n const { value: retryInterval, bytesRead: riBytes } = readVarint(buf, pos);\n pos += riBytes;\n const { value: errorReason, bytesRead: erBytes } = readReasonPhrase(buf, pos);\n pos += erBytes;\n return { message: { type: 'REQUEST_ERROR', requestId, errorCode, retryInterval, errorReason }, finalPos: pos };\n }\n\n case 'SUBSCRIBE':\n case 'TRACK_STATUS': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n const { value: trackName, bytesRead: tnBytes } = readTrackName(buf, pos);\n pos += tnBytes;\n // Validate per §2.4.1\n validateFullTrackName(trackNamespace, trackName);\n const { value: parameters, bytesRead: pBytes } = readParams(buf, pos);\n pos += pBytes;\n return { message: { type: typeName, requestId, trackNamespace, trackName, parameters }, finalPos: pos };\n }\n\n case 'SUBSCRIBE_OK': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackAlias, bytesRead: taBytes } = readVarint(buf, pos);\n pos += taBytes;\n const { value: parameters, bytesRead: pBytes } = readParams(buf, pos);\n pos += pBytes;\n const { value: trackExtensions, bytesRead: extBytes } = readExtensions(buf, pos, payloadEnd);\n pos += extBytes;\n return { message: { type: 'SUBSCRIBE_OK', requestId, trackAlias, parameters, trackExtensions }, finalPos: pos };\n }\n\n case 'REQUEST_UPDATE': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: existingRequestId, bytesRead: eridBytes } = readVarint(buf, pos);\n pos += eridBytes;\n const { value: parameters, bytesRead: pBytes } = readParams(buf, pos);\n pos += pBytes;\n return { message: { type: 'REQUEST_UPDATE', requestId, existingRequestId, parameters }, finalPos: pos };\n }\n\n case 'UNSUBSCRIBE': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n return { message: { type: 'UNSUBSCRIBE', requestId }, finalPos: pos };\n }\n\n case 'PUBLISH': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n const { value: trackName, bytesRead: tnBytes } = readTrackName(buf, pos);\n pos += tnBytes;\n // Validate per §2.4.1\n validateFullTrackName(trackNamespace, trackName);\n const { value: trackAlias, bytesRead: taBytes } = readVarint(buf, pos);\n pos += taBytes;\n const { value: parameters, bytesRead: pBytes } = readParams(buf, pos);\n pos += pBytes;\n const { value: trackExtensions, bytesRead: extBytes } = readExtensions(buf, pos, payloadEnd);\n pos += extBytes;\n return { message: { type: 'PUBLISH', requestId, trackNamespace, trackName, trackAlias, parameters, trackExtensions }, finalPos: pos };\n }\n\n case 'PUBLISH_OK': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: parameters, bytesRead: pBytes } = readParams(buf, pos);\n pos += pBytes;\n return { message: { type: 'PUBLISH_OK', requestId, parameters }, finalPos: pos };\n }\n\n case 'PUBLISH_DONE': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: statusCode, bytesRead: scBytes } = readVarint(buf, pos);\n pos += scBytes;\n const { value: streamCount, bytesRead: stBytes } = readVarint(buf, pos);\n pos += stBytes;\n const { value: errorReason, bytesRead: erBytes } = readReasonPhrase(buf, pos);\n pos += erBytes;\n return { message: { type: 'PUBLISH_DONE', requestId, statusCode, streamCount, errorReason }, finalPos: pos };\n }\n\n case 'FETCH': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: fetchTypeVal, bytesRead: ftBytes } = readVarint(buf, pos);\n pos += ftBytes;\n\n let fetch: StandaloneFetch | JoiningFetch;\n\n if (fetchTypeVal === 0x1n) {\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n const { value: trackName, bytesRead: tnBytes } = readTrackName(buf, pos);\n pos += tnBytes;\n // Validate per §2.4.1\n validateFullTrackName(trackNamespace, trackName);\n const { value: startLocation, bytesRead: slBytes } = readLocation(buf, pos);\n pos += slBytes;\n const { value: endLocation, bytesRead: elBytes } = readLocation(buf, pos);\n pos += elBytes;\n fetch = { fetchType: 0x1, trackNamespace, trackName, startLocation, endLocation };\n } else if (fetchTypeVal === 0x2n || fetchTypeVal === 0x3n) {\n const { value: joiningRequestId, bytesRead: jridBytes } = readVarint(buf, pos);\n pos += jridBytes;\n const { value: joiningStart, bytesRead: jsBytes } = readVarint(buf, pos);\n pos += jsBytes;\n fetch = { fetchType: Number(fetchTypeVal) as 0x2 | 0x3, joiningRequestId, joiningStart };\n } else {\n throw new ProtocolViolationError(`Invalid Fetch Type: 0x${fetchTypeVal.toString(16)}`);\n }\n\n const { value: parameters, bytesRead: pBytes } = readParams(buf, pos);\n pos += pBytes;\n return { message: { type: 'FETCH', requestId, fetch, parameters }, finalPos: pos };\n }\n\n case 'FETCH_OK': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: endOfTrack } = readUint8(buf, pos);\n pos += 1;\n const { value: endLocation, bytesRead: elBytes } = readLocation(buf, pos);\n pos += elBytes;\n const { value: parameters, bytesRead: pBytes } = readParams(buf, pos);\n pos += pBytes;\n const { value: trackExtensions, bytesRead: extBytes } = readExtensions(buf, pos, payloadEnd);\n pos += extBytes;\n return { message: { type: 'FETCH_OK', requestId, endOfTrack, endLocation, parameters, trackExtensions }, finalPos: pos };\n }\n\n case 'FETCH_CANCEL': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n return { message: { type: 'FETCH_CANCEL', requestId }, finalPos: pos };\n }\n\n case 'PUBLISH_NAMESPACE': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n // Validate per §2.4.1 (full namespace: 1-32 fields)\n validateTrackNamespace(trackNamespace);\n const { value: parameters, bytesRead: pBytes } = readParams(buf, pos);\n pos += pBytes;\n return { message: { type: 'PUBLISH_NAMESPACE', requestId, trackNamespace, parameters }, finalPos: pos };\n }\n\n case 'NAMESPACE': {\n const { value: trackNamespaceSuffix, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n // Validate suffix: ≤32 fields, each ≥1 byte, total ≤4096 bytes\n validateTrackNamespaceSuffix(trackNamespaceSuffix);\n return { message: { type: 'NAMESPACE', trackNamespaceSuffix }, finalPos: pos };\n }\n\n case 'PUBLISH_NAMESPACE_DONE': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n return { message: { type: 'PUBLISH_NAMESPACE_DONE', requestId }, finalPos: pos };\n }\n\n case 'NAMESPACE_DONE': {\n const { value: trackNamespaceSuffix, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n // Validate suffix: ≤32 fields, each ≥1 byte, total ≤4096 bytes\n validateTrackNamespaceSuffix(trackNamespaceSuffix);\n return { message: { type: 'NAMESPACE_DONE', trackNamespaceSuffix }, finalPos: pos };\n }\n\n case 'PUBLISH_NAMESPACE_CANCEL': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: errorCode, bytesRead: ecBytes } = readVarint(buf, pos);\n pos += ecBytes;\n const { value: errorReason, bytesRead: erBytes } = readReasonPhrase(buf, pos);\n pos += erBytes;\n return { message: { type: 'PUBLISH_NAMESPACE_CANCEL', requestId, errorCode, errorReason }, finalPos: pos };\n }\n\n case 'SUBSCRIBE_NAMESPACE': {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackNamespacePrefix, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n // Validate per §9.25 (prefix: 0-32 fields)\n validateTrackNamespacePrefix(trackNamespacePrefix);\n const { value: subscribeOptions, bytesRead: soBytes } = readVarint(buf, pos);\n pos += soBytes;\n const { value: parameters, bytesRead: pBytes } = readParams(buf, pos);\n pos += pBytes;\n return { message: { type: 'SUBSCRIBE_NAMESPACE', requestId, trackNamespacePrefix, subscribeOptions, parameters }, finalPos: pos };\n }\n\n // Draft-14-only types — not decodable by draft-16 decoder\n // (these wire codes map to different message types in draft-16)\n case 'UNSUBSCRIBE_NAMESPACE':\n case 'PUBLISH_NAMESPACE_OK':\n case 'PUBLISH_NAMESPACE_ERROR':\n case 'PUBLISH_ERROR':\n throw new ProtocolViolationError(`Draft-14-only message type \"${typeName}\" cannot be decoded by draft-16 decoder`);\n // Draft-18-only types — not decodable by the draft-14/16 decoder\n case 'SUBSCRIBE_TRACKS':\n case 'PUBLISH_BLOCKED':\n case 'PUBLISH_STATE_NOTIFY':\n throw new ProtocolViolationError(`Draft-18-only message type \"${typeName}\" cannot be decoded by draft-16 decoder`);\n }\n}\n","/**\n * Draft-14 control message codec.\n *\n * Encodes outbound ControlMessages to draft-14 wire format and decodes\n * inbound draft-14 wire bytes, normalizing most messages to draft-16\n * ControlMessage types at decode time.\n *\n * Key differences from draft-16:\n * - KVP uses absolute type values (not delta-encoded) — §1.4.2\n * - CLIENT_SETUP prepends version list before params — §9.3\n * - SERVER_SETUP prepends selected version before params — §9.3\n * - SUBSCRIBE/SUBSCRIBE_OK have inline fields → normalized to params — §9.7, §9.8\n * - SUBSCRIBE_ERROR/FETCH_ERROR → normalized to RequestError — §9.9, §9.18\n * - TRACK_STATUS_OK/ERROR → normalized to RequestOk/RequestError — §9.21, §9.22\n * - SUBSCRIBE_NAMESPACE_OK/ERROR → normalized to RequestOk/RequestError — §9.29, §9.30\n * - PUBLISH_NAMESPACE_DONE/CANCEL use namespace tuples (not requestIds) — §9.26, §9.27\n * - SUBSCRIBE_NAMESPACE has no subscribeOptions field — §9.28\n * - PUBLISH/PUBLISH_OK use varint length instead of uint16 — §9.13, §9.14\n * - FETCH has inline Priority/GroupOrder — §9.16\n * - FETCH_OK has inline GroupOrder — §9.17\n *\n * @see draft-ietf-moq-transport-14\n * @module\n */\n\nimport type { ControlCodec } from './codec.js';\nimport type {\n ControlMessage,\n Parameters,\n Subscribe,\n SubscribeOk,\n ClientSetup,\n ServerSetup,\n RequestErrorMsg,\n RequestErrorKind,\n Fetch,\n RequestUpdate,\n TrackExtensions,\n Publish,\n PublishOk,\n PublishError,\n PublishDone,\n} from './messages.js';\nimport type { Varint } from '../primitives/varint.js';\nimport type { Location } from '../primitives/location.js';\nimport { varint, readVarint, writeVarint, varintEncodingLength } from '../primitives/varint.js';\nimport {\n readLengthPrefixedBytes, writeLengthPrefixedBytes, lengthPrefixedBytesEncodingLength,\n readTuple, writeTuple, tupleEncodingLength,\n readUint8,\n validateTrackNamespace,\n validateTrackNamespacePrefix,\n} from '../primitives/bytes.js';\nimport {\n readKvpListAbsolute, writeKvpListAbsolute,\n kvpListAbsoluteEncodingLength, kvpListEntryCount,\n} from '../primitives/kvp.js';\nimport { readLocation, writeLocation, locationEncodingLength } from '../primitives/location.js';\nimport { readReasonPhrase, writeReasonPhrase, reasonPhraseEncodingLength } from '../primitives/reason.js';\nimport { MessageParam } from './parameters.js';\nimport { toKvpParams } from './kvp-params.js';\nimport { ProtocolViolationError } from '../errors.js';\n\n// ─── Draft-14 Wire Codes ─────────────────────────────────────────────\n// @see draft-ietf-moq-transport-14 §9\n\nconst D14 = {\n // Outbound (subscriber sends)\n CLIENT_SETUP: 0x20,\n SUBSCRIBE: 0x03,\n SUBSCRIBE_UPDATE: 0x02,\n SUBSCRIBE_NAMESPACE: 0x11,\n UNSUBSCRIBE_NAMESPACE: 0x14,\n UNSUBSCRIBE: 0x0a,\n FETCH: 0x16,\n FETCH_CANCEL: 0x17,\n TRACK_STATUS: 0x0d,\n PUBLISH_NAMESPACE_OK: 0x07,\n PUBLISH_NAMESPACE_ERROR: 0x08,\n\n // Inbound (subscriber receives)\n SERVER_SETUP: 0x21,\n SUBSCRIBE_OK: 0x04,\n SUBSCRIBE_ERROR: 0x05,\n FETCH_OK: 0x18,\n FETCH_ERROR: 0x19,\n TRACK_STATUS_OK: 0x0e,\n TRACK_STATUS_ERROR: 0x0f,\n SUBSCRIBE_NAMESPACE_OK: 0x12,\n SUBSCRIBE_NAMESPACE_ERROR: 0x13,\n PUBLISH_NAMESPACE: 0x06,\n PUBLISH_NAMESPACE_DONE: 0x09,\n PUBLISH_NAMESPACE_CANCEL: 0x0c,\n PUBLISH_DONE: 0x0b,\n GOAWAY: 0x10,\n MAX_REQUEST_ID: 0x15,\n REQUESTS_BLOCKED: 0x1a,\n\n // Varint-length messages\n PUBLISH: 0x1d,\n PUBLISH_OK: 0x1e,\n PUBLISH_ERROR: 0x1f,\n} as const;\n\n/** Messages that use varint length instead of uint16. */\nconst VARINT_LENGTH_TYPES: Set<number> = new Set([D14.PUBLISH, D14.PUBLISH_OK, D14.PUBLISH_ERROR]);\n\n// ─── Draft-14 Codec Implementation ──────────────────────────────────\n\nexport class Draft14Codec implements ControlCodec {\n readonly version = 14 as const;\n\n // ─── encode ───────────────────────────────────────────────────────\n\n encode(msg: ControlMessage): Uint8Array {\n switch (msg.type) {\n case 'CLIENT_SETUP': return this.encodeClientSetup(msg);\n case 'SERVER_SETUP': return this.encodeServerSetup(msg);\n case 'SUBSCRIBE': return this.encodeSubscribe(msg);\n case 'REQUEST_UPDATE': return this.encodeSubscribeUpdate(msg);\n case 'SUBSCRIBE_NAMESPACE': return this.encodeSubscribeNamespace(msg);\n case 'UNSUBSCRIBE_NAMESPACE': return this.encodeUnsubscribeNamespace(msg);\n case 'UNSUBSCRIBE': return this.encodeSimpleRequestId(D14.UNSUBSCRIBE, msg.requestId);\n case 'FETCH': return this.encodeFetch(msg);\n case 'FETCH_CANCEL': return this.encodeSimpleRequestId(D14.FETCH_CANCEL, msg.requestId);\n case 'TRACK_STATUS': return this.encodeTrackStatus(msg);\n case 'PUBLISH_NAMESPACE': return this.encodePublishNamespace(msg);\n case 'PUBLISH_NAMESPACE_OK': return this.encodeSimpleRequestId(D14.PUBLISH_NAMESPACE_OK, msg.requestId);\n case 'PUBLISH_NAMESPACE_ERROR': return this.encodePublishNamespaceError(msg);\n case 'PUBLISH': return this.encodePublish(msg);\n case 'PUBLISH_OK': return this.encodePublishOk(msg);\n case 'PUBLISH_ERROR': return this.encodePublishError(msg);\n case 'SUBSCRIBE_OK': return this.encodeSubscribeOk(msg);\n case 'PUBLISH_DONE': return this.encodePublishDone(msg);\n case 'PUBLISH_NAMESPACE_DONE': return this.encodeNamespaceTuple(D14.PUBLISH_NAMESPACE_DONE, msg.trackNamespace!);\n case 'REQUEST_ERROR': return this.encodeRequestError(msg);\n case 'REQUEST_OK':\n throw new Error('Draft-14 does not have generic REQUEST_OK — use specific OK types');\n default:\n throw new Error(`Draft14Codec cannot encode message type \"${msg.type}\"`);\n }\n }\n\n // ─── decode ───────────────────────────────────────────────────────\n\n decode(buf: Uint8Array, offset: number): { message: ControlMessage; bytesRead: number } {\n let pos = offset;\n\n const { value: msgType, bytesRead: typeBytes } = readVarint(buf, pos);\n pos += typeBytes;\n\n const typeNum = Number(msgType);\n const isVarintLen = VARINT_LENGTH_TYPES.has(typeNum);\n\n let payloadLen: number;\n let headerOverhead: number;\n\n if (isVarintLen) {\n const { value: lenVal, bytesRead: lenBytes } = readVarint(buf, pos);\n pos += lenBytes;\n payloadLen = Number(lenVal);\n headerOverhead = typeBytes + lenBytes;\n } else {\n if (pos + 2 > buf.length) throw new RangeError('Buffer too short for message length field');\n payloadLen = (buf[pos]! << 8) | buf[pos + 1]!;\n pos += 2;\n headerOverhead = typeBytes + 2;\n }\n\n const payloadEnd = pos + payloadLen;\n if (payloadEnd > buf.length) {\n throw new RangeError(`Buffer too short: need ${payloadLen} bytes, have ${buf.length - pos}`);\n }\n\n const message = this.decodePayload(typeNum, buf, pos, payloadEnd);\n return { message, bytesRead: headerOverhead + payloadLen };\n }\n\n // ─── peekFrameSize ────────────────────────────────────────────────\n\n peekFrameSize(buf: Uint8Array): number | undefined {\n if (buf.length === 0) return undefined;\n\n // Determine type varint length from first byte\n const first = buf[0]!;\n const lengthFlag = first >> 6;\n const typeVarintLen = (1 << lengthFlag) as 1 | 2 | 4 | 8;\n\n if (buf.length < typeVarintLen) return undefined;\n\n // Read the actual type value to check if it uses varint length\n const { value: msgType } = readVarint(buf, 0);\n const typeNum = Number(msgType);\n\n if (VARINT_LENGTH_TYPES.has(typeNum)) {\n // Varint length\n if (buf.length < typeVarintLen + 1) return undefined;\n const lenFirst = buf[typeVarintLen]!;\n const lenFlag = lenFirst >> 6;\n const lenVarintLen = (1 << lenFlag) as 1 | 2 | 4 | 8;\n if (buf.length < typeVarintLen + lenVarintLen) return undefined;\n const { value: payloadLen } = readVarint(buf, typeVarintLen);\n return typeVarintLen + lenVarintLen + Number(payloadLen);\n } else {\n // uint16 length\n const headerLen = typeVarintLen + 2;\n if (buf.length < headerLen) return undefined;\n const payloadLen = (buf[typeVarintLen]! << 8) | buf[typeVarintLen + 1]!;\n return headerLen + payloadLen;\n }\n }\n\n // ─── Encode Helpers ───────────────────────────────────────────────\n\n private frame16(typeCode: number, payload: Uint8Array): Uint8Array {\n const typeVarint = varint(typeCode);\n const typeLen = varintEncodingLength(typeVarint);\n const total = typeLen + 2 + payload.length;\n const buf = new Uint8Array(total);\n let pos = 0;\n pos += writeVarint(typeVarint, buf, pos);\n buf[pos++] = (payload.length >> 8) & 0xff;\n buf[pos++] = payload.length & 0xff;\n buf.set(payload, pos);\n return buf;\n }\n\n private writeParams(params: Parameters, buf: Uint8Array, offset: number): number {\n let pos = offset;\n const kvp = toKvpParams(params);\n const count = kvpListEntryCount(kvp);\n pos += writeVarint(varint(count), buf, pos);\n pos += writeKvpListAbsolute(kvp, buf, pos);\n return pos - offset;\n }\n\n private paramsLength(params: Parameters): number {\n const kvp = toKvpParams(params);\n const count = kvpListEntryCount(kvp);\n return varintEncodingLength(varint(count)) + kvpListAbsoluteEncodingLength(kvp);\n }\n\n /**\n * Assert a value fits in an 8-bit wire field.\n *\n * Draft-14 declares SUBSCRIBER_PRIORITY (§9.7), FORWARD (§9.7), GROUP_ORDER\n * (§9.2.2.4), and related fields as 8-bit inline integers. The internal\n * model carries them as Varint, so out-of-range input would silently\n * truncate on encode (e.g. varint(300) → 0x2C). Reject loudly at the\n * encode boundary so the peer never sees a corrupted wire value.\n *\n * Policy: validate-on-encode. Mirrors the validate-on-decode check in\n * session.ts (§9.2.2.3 SUBSCRIBER_PRIORITY range enforcement).\n */\n private assertUint8(value: number, field: string, msgType: string): void {\n if (!Number.isInteger(value) || value < 0 || value > 255) {\n throw new ProtocolViolationError(\n `${msgType}: ${field} must be 0-255 for 8-bit wire encoding, got ${value}`,\n );\n }\n }\n\n private encodeSimpleRequestId(typeCode: number, requestId: bigint): Uint8Array {\n const payloadLen = varintEncodingLength(requestId);\n const payload = new Uint8Array(payloadLen);\n writeVarint(requestId, payload, 0);\n return this.frame16(typeCode, payload);\n }\n\n private encodeClientSetup(msg: ClientSetup): Uint8Array {\n // Draft-14 §9.3: Number of Versions (i), Versions (i)..., Params\n // We advertise draft-14 version: 0xff00000e\n const draftVersion = varint(0xff00000e);\n const payloadLen =\n varintEncodingLength(varint(1)) + // Number of Supported Versions\n varintEncodingLength(draftVersion) + // Version value\n this.paramsLength(msg.parameters);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(varint(1), payload, pos); // 1 version\n pos += writeVarint(draftVersion, payload, pos); // draft-14\n pos += this.writeParams(msg.parameters, payload, pos);\n\n return this.frame16(D14.CLIENT_SETUP, payload);\n }\n\n private encodeServerSetup(msg: ServerSetup): Uint8Array {\n // Draft-14 §9.3: Selected Version (i), Params. Unlike CLIENT_SETUP (which\n // advertises a version list), the server echoes the single negotiated\n // version. This mirrors the SERVER_SETUP decode path below and differs from\n // Playa's draft-16 codec, which frames both SETUPs as params-only.\n const selectedVersion = varint(0xff00000e); // draft-14\n const payloadLen =\n varintEncodingLength(selectedVersion) + // Selected Version\n this.paramsLength(msg.parameters);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(selectedVersion, payload, pos);\n pos += this.writeParams(msg.parameters, payload, pos);\n\n return this.frame16(D14.SERVER_SETUP, payload);\n }\n\n /**\n * Encode a (normalized) REQUEST_ERROR back to its specific draft-14 wire type.\n *\n * Draft-14 has no generic REQUEST_ERROR; it splits into SUBSCRIBE_ERROR (§9.9),\n * FETCH_ERROR (§9.18), TRACK_STATUS_ERROR (§9.22), and SUBSCRIBE_NAMESPACE_ERROR\n * (§9.30) — all sharing the identical [Request ID, Error Code, Reason] payload\n * and differing only by type code. The session stamps `requestKind` so we\n * recover the type WITHOUT guessing from the Request ID. Without that context\n * we cannot disambiguate, so we throw (loudly) rather than mis-encode.\n */\n private encodeRequestError(msg: RequestErrorMsg): Uint8Array {\n const typeByKind: Record<RequestErrorKind, number> = {\n SUBSCRIBE: D14.SUBSCRIBE_ERROR,\n FETCH: D14.FETCH_ERROR,\n TRACK_STATUS: D14.TRACK_STATUS_ERROR,\n SUBSCRIBE_NAMESPACE: D14.SUBSCRIBE_NAMESPACE_ERROR,\n };\n if (msg.requestKind === undefined) {\n throw new Error('Draft-14 does not have generic REQUEST_ERROR — use specific error types');\n }\n const typeCode = typeByKind[msg.requestKind];\n\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n varintEncodingLength(msg.errorCode) +\n reasonPhraseEncodingLength(msg.errorReason);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeVarint(msg.errorCode, payload, pos);\n pos += writeReasonPhrase(msg.errorReason, payload, pos);\n\n return this.frame16(typeCode, payload);\n }\n\n private encodeSubscribe(msg: Subscribe): Uint8Array {\n validateTrackNamespace(msg.trackNamespace);\n // Draft-14 §9.7: inline Priority, GroupOrder, Forward, FilterType, [StartLoc], [EndGroup]\n const priority = Number(this.extractParamVarint(msg.parameters, MessageParam.SUBSCRIBER_PRIORITY) ?? 128n);\n const groupOrder = Number(this.extractParamVarint(msg.parameters, MessageParam.GROUP_ORDER) ?? 0n);\n const forward = Number(this.extractParamVarint(msg.parameters, MessageParam.FORWARD) ?? 1n);\n this.assertUint8(priority, 'SUBSCRIBER_PRIORITY', 'SUBSCRIBE');\n this.assertUint8(groupOrder, 'GROUP_ORDER', 'SUBSCRIBE');\n this.assertUint8(forward, 'FORWARD', 'SUBSCRIBE');\n\n // SUBSCRIPTION_FILTER is a bytes parameter (type 0x21 is odd).\n // Parse the draft-16 filter structure: FilterType (i), [StartGroup (i), StartObject (i)], [EndGroup (i)]\n const filter = this.extractFilter(msg.parameters);\n\n // Build remaining params (without extracted inline fields)\n const remainingParams = this.cloneParamsWithout(msg.parameters, [\n MessageParam.SUBSCRIBER_PRIORITY,\n MessageParam.GROUP_ORDER,\n MessageParam.FORWARD,\n MessageParam.SUBSCRIPTION_FILTER,\n ]);\n\n // Calculate filter-specific fields length\n let filterFieldsLen = 0;\n if (filter.startLocation) {\n filterFieldsLen += locationEncodingLength(filter.startLocation);\n }\n if (filter.endGroup !== undefined) {\n filterFieldsLen += varintEncodingLength(filter.endGroup);\n }\n\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n tupleEncodingLength(msg.trackNamespace) +\n lengthPrefixedBytesEncodingLength(msg.trackName) +\n 1 + // Subscriber Priority (8)\n 1 + // Group Order (8)\n 1 + // Forward (8)\n varintEncodingLength(filter.filterType) +\n filterFieldsLen +\n this.paramsLength(remainingParams);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeTuple(msg.trackNamespace, payload, pos);\n pos += writeLengthPrefixedBytes(msg.trackName, payload, pos);\n payload[pos++] = priority & 0xff;\n payload[pos++] = groupOrder & 0xff;\n payload[pos++] = forward & 0xff;\n pos += writeVarint(filter.filterType, payload, pos);\n\n // Filter-specific fields (§9.7: Start Location, End Group)\n if (filter.startLocation) {\n pos += writeLocation(filter.startLocation, payload, pos);\n }\n if (filter.endGroup !== undefined) {\n pos += writeVarint(filter.endGroup, payload, pos);\n }\n\n pos += this.writeParams(remainingParams, payload, pos);\n\n return this.frame16(D14.SUBSCRIBE, payload);\n }\n\n private encodeFetch(msg: Fetch): Uint8Array {\n if (msg.fetch.fetchType === 0x1) {\n validateTrackNamespace(msg.fetch.trackNamespace);\n }\n // Draft-14 §9.16: inline Subscriber Priority (8), Group Order (8) before FetchType\n const priority = Number(this.extractParamVarint(msg.parameters, MessageParam.SUBSCRIBER_PRIORITY) ?? 128n);\n const groupOrder = Number(this.extractParamVarint(msg.parameters, MessageParam.GROUP_ORDER) ?? 0n);\n this.assertUint8(priority, 'SUBSCRIBER_PRIORITY', 'FETCH');\n this.assertUint8(groupOrder, 'GROUP_ORDER', 'FETCH');\n\n const remainingParams = this.cloneParamsWithout(msg.parameters, [\n MessageParam.SUBSCRIBER_PRIORITY,\n MessageParam.GROUP_ORDER,\n ]);\n\n let fetchFieldsLen = varintEncodingLength(varint(msg.fetch.fetchType));\n if (msg.fetch.fetchType === 0x1) {\n fetchFieldsLen +=\n tupleEncodingLength(msg.fetch.trackNamespace) +\n lengthPrefixedBytesEncodingLength(msg.fetch.trackName) +\n locationEncodingLength(msg.fetch.startLocation) +\n locationEncodingLength(msg.fetch.endLocation);\n } else {\n fetchFieldsLen +=\n varintEncodingLength(msg.fetch.joiningRequestId) +\n varintEncodingLength(msg.fetch.joiningStart);\n }\n\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n 1 + // Subscriber Priority (8)\n 1 + // Group Order (8)\n fetchFieldsLen +\n this.paramsLength(remainingParams);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n payload[pos++] = priority & 0xff;\n payload[pos++] = groupOrder & 0xff;\n pos += writeVarint(varint(msg.fetch.fetchType), payload, pos);\n\n if (msg.fetch.fetchType === 0x1) {\n pos += writeTuple(msg.fetch.trackNamespace, payload, pos);\n pos += writeLengthPrefixedBytes(msg.fetch.trackName, payload, pos);\n pos += writeLocation(msg.fetch.startLocation, payload, pos);\n pos += writeLocation(msg.fetch.endLocation, payload, pos);\n } else {\n pos += writeVarint(msg.fetch.joiningRequestId, payload, pos);\n pos += writeVarint(msg.fetch.joiningStart, payload, pos);\n }\n\n pos += this.writeParams(remainingParams, payload, pos);\n\n return this.frame16(D14.FETCH, payload);\n }\n\n private encodeTrackStatus(msg: ControlMessage & { type: 'TRACK_STATUS' }): Uint8Array {\n validateTrackNamespace(msg.trackNamespace);\n // Same wire format as draft-16 but with absolute KVP\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n tupleEncodingLength(msg.trackNamespace) +\n lengthPrefixedBytesEncodingLength(msg.trackName) +\n this.paramsLength(msg.parameters);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeTuple(msg.trackNamespace, payload, pos);\n pos += writeLengthPrefixedBytes(msg.trackName, payload, pos);\n pos += this.writeParams(msg.parameters, payload, pos);\n\n return this.frame16(D14.TRACK_STATUS, payload);\n }\n\n private encodeSubscribeNamespace(msg: ControlMessage & { type: 'SUBSCRIBE_NAMESPACE' }): Uint8Array {\n validateTrackNamespacePrefix(msg.trackNamespacePrefix);\n // Draft-14 §9.28: No subscribeOptions field\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n tupleEncodingLength(msg.trackNamespacePrefix) +\n this.paramsLength(msg.parameters);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeTuple(msg.trackNamespacePrefix, payload, pos);\n pos += this.writeParams(msg.parameters, payload, pos);\n\n return this.frame16(D14.SUBSCRIBE_NAMESPACE, payload);\n }\n\n private encodeUnsubscribeNamespace(msg: ControlMessage & { type: 'UNSUBSCRIBE_NAMESPACE' }): Uint8Array {\n validateTrackNamespacePrefix(msg.trackNamespacePrefix);\n // Draft-14 §9.31: Track Namespace Prefix (tuple)\n const payloadLen = tupleEncodingLength(msg.trackNamespacePrefix);\n const payload = new Uint8Array(payloadLen);\n writeTuple(msg.trackNamespacePrefix, payload, 0);\n return this.frame16(D14.UNSUBSCRIBE_NAMESPACE, payload);\n }\n\n private encodePublishNamespace(msg: ControlMessage & { type: 'PUBLISH_NAMESPACE' }): Uint8Array {\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n tupleEncodingLength(msg.trackNamespace) +\n this.paramsLength(msg.parameters);\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeTuple(msg.trackNamespace, payload, pos);\n pos += this.writeParams(msg.parameters, payload, pos);\n return this.frame16(D14.PUBLISH_NAMESPACE, payload);\n }\n\n private encodeNamespaceTuple(type: number, namespace: Uint8Array[]): Uint8Array {\n const payloadLen = tupleEncodingLength(namespace);\n const payload = new Uint8Array(payloadLen);\n writeTuple(namespace, payload, 0);\n return this.frame16(type, payload);\n }\n\n private encodePublishNamespaceError(msg: ControlMessage & { type: 'PUBLISH_NAMESPACE_ERROR' }): Uint8Array {\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n varintEncodingLength(msg.errorCode) +\n reasonPhraseEncodingLength(msg.errorReason);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeVarint(msg.errorCode, payload, pos);\n pos += writeReasonPhrase(msg.errorReason, payload, pos);\n\n return this.frame16(D14.PUBLISH_NAMESPACE_ERROR, payload);\n }\n\n /** Encode draft-14 PUBLISH with its state fields inlined on the wire (§9.13). */\n private encodePublish(msg: Publish): Uint8Array {\n validateTrackNamespace(msg.trackNamespace);\n\n const trackProperties = msg.trackProperties ?? msg.trackExtensions ?? new Map();\n if (trackProperties.size > 0) {\n throw new ProtocolViolationError('PUBLISH Track Properties are not defined in draft-14');\n }\n\n const groupOrder = Number(\n this.extractSingleVarintParam(\n msg.parameters,\n MessageParam.GROUP_ORDER,\n 'GROUP_ORDER',\n 'PUBLISH',\n ) ?? 1n,\n );\n if (groupOrder !== 1 && groupOrder !== 2) {\n throw new ProtocolViolationError(\n `PUBLISH: GROUP_ORDER must be Ascending (0x1) or Descending (0x2), got ${groupOrder}`,\n );\n }\n\n const forward = Number(\n this.extractSingleVarintParam(\n msg.parameters,\n MessageParam.FORWARD,\n 'FORWARD',\n 'PUBLISH',\n ) ?? 1n,\n );\n if (forward !== 0 && forward !== 1) {\n throw new ProtocolViolationError(`PUBLISH: FORWARD must be 0 or 1, got ${forward}`);\n }\n\n let largestLocation: Location | undefined;\n const largestValues = msg.parameters.get(MessageParam.LARGEST_OBJECT);\n if (largestValues !== undefined) {\n const encoded = largestValues.length === 1 ? largestValues[0] : undefined;\n if (!(encoded instanceof Uint8Array)) {\n throw new ProtocolViolationError(\n 'PUBLISH: LARGEST_OBJECT must contain exactly one Location byte string',\n );\n }\n try {\n const decoded = readLocation(encoded, 0);\n if (decoded.bytesRead !== encoded.byteLength) {\n throw new RangeError('trailing bytes');\n }\n largestLocation = decoded.value;\n } catch {\n throw new ProtocolViolationError(\n 'PUBLISH: LARGEST_OBJECT must contain exactly one Location',\n );\n }\n }\n\n const remainingParams = this.cloneParamsWithout(msg.parameters, [\n MessageParam.GROUP_ORDER,\n MessageParam.LARGEST_OBJECT,\n MessageParam.FORWARD,\n ]);\n const largestLength = largestLocation === undefined\n ? 0\n : locationEncodingLength(largestLocation);\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n tupleEncodingLength(msg.trackNamespace) +\n lengthPrefixedBytesEncodingLength(msg.trackName) +\n varintEncodingLength(msg.trackAlias) +\n 1 + // Group Order\n 1 + // Content Exists\n largestLength +\n 1 + // Forward\n this.paramsLength(remainingParams);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeTuple(msg.trackNamespace, payload, pos);\n pos += writeLengthPrefixedBytes(msg.trackName, payload, pos);\n pos += writeVarint(msg.trackAlias, payload, pos);\n payload[pos++] = groupOrder;\n payload[pos++] = largestLocation === undefined ? 0 : 1;\n if (largestLocation !== undefined) {\n pos += writeLocation(largestLocation, payload, pos);\n }\n payload[pos++] = forward;\n pos += this.writeParams(remainingParams, payload, pos);\n\n return this.frameVarint(D14.PUBLISH, payload);\n }\n\n // ─── Decode Helpers ───────────────────────────────────────────────\n\n private decodePayload(typeNum: number, buf: Uint8Array, offset: number, payloadEnd: number): ControlMessage {\n let pos = offset;\n\n switch (typeNum) {\n // ── Setup ──\n case D14.CLIENT_SETUP: {\n // Draft-14 §9.3: versions list then params\n const { value: numVersions, bytesRead: nvBytes } = readVarint(buf, pos);\n pos += nvBytes;\n for (let i = 0; i < Number(numVersions); i++) {\n const { bytesRead: vBytes } = readVarint(buf, pos);\n pos += vBytes;\n }\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n this.assertConsumed(pos, payloadEnd, 'CLIENT_SETUP');\n return { type: 'CLIENT_SETUP', parameters };\n }\n\n case D14.SERVER_SETUP: {\n // Draft-14 §9.3: selected version then params\n const { bytesRead: vBytes } = readVarint(buf, pos);\n pos += vBytes;\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n this.assertConsumed(pos, payloadEnd, 'SERVER_SETUP');\n return { type: 'SERVER_SETUP', parameters };\n }\n\n // ── Subscribe ──\n case D14.SUBSCRIBE: {\n // Draft-14 §9.7: inline fields\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n const { value: trackName, bytesRead: tnBytes } = readLengthPrefixedBytes(buf, pos);\n pos += tnBytes;\n const priority = buf[pos++]!;\n const groupOrder = buf[pos++]!;\n const forward = buf[pos++]!;\n const { value: filterType, bytesRead: ftBytes } = readVarint(buf, pos);\n pos += ftBytes;\n\n // Read filter-specific fields and encode as SUBSCRIPTION_FILTER bytes\n // (draft-16 format) so the session validator can structurally validate.\n let startLoc: Location | undefined;\n let endGroupVal: Varint | undefined;\n if (filterType === 0x3n || filterType === 0x4n) {\n const { value: sl, bytesRead: slBytes } = readLocation(buf, pos);\n pos += slBytes;\n startLoc = sl;\n }\n if (filterType === 0x4n) {\n const { value: eg, bytesRead: egBytes } = readVarint(buf, pos);\n pos += egBytes;\n endGroupVal = eg;\n }\n\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n\n // Normalize inline fields → parameters\n parameters.set(MessageParam.SUBSCRIBER_PRIORITY, [varint(priority)]);\n parameters.set(MessageParam.GROUP_ORDER, [varint(groupOrder)]);\n parameters.set(MessageParam.FORWARD, [varint(forward)]);\n\n // Encode filter as bytes (draft-16 SUBSCRIPTION_FILTER wire format)\n // so session structural validation works uniformly.\n const filterBytes = this.encodeFilterBytes(varint(filterType), startLoc, endGroupVal);\n parameters.set(MessageParam.SUBSCRIPTION_FILTER, [filterBytes]);\n\n this.assertConsumed(pos, payloadEnd, 'SUBSCRIBE');\n return { type: 'SUBSCRIBE', requestId, trackNamespace, trackName, parameters };\n }\n\n case D14.SUBSCRIBE_OK: {\n // Draft-14 §9.8: inline fields\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackAlias, bytesRead: taBytes } = readVarint(buf, pos);\n pos += taBytes;\n const { value: expires, bytesRead: exBytes } = readVarint(buf, pos);\n pos += exBytes;\n const groupOrder = buf[pos++]!;\n const contentExists = buf[pos++]!;\n\n // §9.8: \"Any other value is a protocol error and MUST terminate\n // the session with a PROTOCOL_VIOLATION\"\n if (contentExists !== 0 && contentExists !== 1) {\n throw new ProtocolViolationError(\n `SUBSCRIBE_OK Content Exists has invalid value ${contentExists} (must be 0 or 1)`,\n );\n }\n\n let largestLocation: Location | undefined;\n if (contentExists === 1) {\n const { value: ll, bytesRead: llBytes } = readLocation(buf, pos);\n pos += llBytes;\n largestLocation = ll;\n }\n\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n\n // Normalize inline fields → parameters\n parameters.set(MessageParam.EXPIRES, [expires]);\n parameters.set(MessageParam.GROUP_ORDER, [varint(groupOrder)]);\n if (largestLocation !== undefined) {\n // §9.2.2.7: LARGEST_OBJECT is a Location structure (group + object bytes).\n // Encode as bytes so session validateLargestObject() works correctly.\n const locBuf = new Uint8Array(locationEncodingLength(largestLocation));\n writeLocation(largestLocation, locBuf, 0);\n parameters.set(MessageParam.LARGEST_OBJECT, [locBuf]);\n }\n\n this.assertConsumed(pos, payloadEnd, 'SUBSCRIBE_OK');\n // No trackExtensions in draft-14\n const trackExtensions: TrackExtensions = new Map();\n return { type: 'SUBSCRIBE_OK', requestId, trackAlias, parameters, trackExtensions };\n }\n\n // ── Error types normalized to REQUEST_ERROR ──\n case D14.SUBSCRIBE_ERROR:\n case D14.FETCH_ERROR:\n case D14.TRACK_STATUS_ERROR:\n case D14.SUBSCRIBE_NAMESPACE_ERROR: {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: errorCode, bytesRead: ecBytes } = readVarint(buf, pos);\n pos += ecBytes;\n const { value: errorReason, bytesRead: erBytes } = readReasonPhrase(buf, pos);\n pos += erBytes;\n this.assertConsumed(pos, payloadEnd, 'ERROR');\n return {\n type: 'REQUEST_ERROR',\n requestId,\n errorCode,\n retryInterval: varint(0),\n errorReason,\n };\n }\n\n // ── OK types normalized to REQUEST_OK ──\n case D14.TRACK_STATUS_OK: {\n // Same wire format as SUBSCRIBE_OK but normalized to REQUEST_OK\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { bytesRead: taBytes } = readVarint(buf, pos); // Track Alias (always 0)\n pos += taBytes;\n const { value: expires, bytesRead: exBytes } = readVarint(buf, pos);\n pos += exBytes;\n const groupOrder = buf[pos++]!;\n const tsContentExists = buf[pos++]!;\n if (tsContentExists !== 0 && tsContentExists !== 1) {\n throw new ProtocolViolationError(\n `TRACK_STATUS_OK Content Exists has invalid value ${tsContentExists} (must be 0 or 1)`,\n );\n }\n if (tsContentExists === 1) {\n const { bytesRead: llBytes } = readLocation(buf, pos);\n pos += llBytes;\n }\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n parameters.set(MessageParam.EXPIRES, [expires]);\n parameters.set(MessageParam.GROUP_ORDER, [varint(groupOrder)]);\n this.assertConsumed(pos, payloadEnd, 'TRACK_STATUS_OK');\n return { type: 'REQUEST_OK', requestId, parameters };\n }\n\n case D14.SUBSCRIBE_NAMESPACE_OK: {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n this.assertConsumed(pos, payloadEnd, 'SUBSCRIBE_NAMESPACE_OK');\n return { type: 'REQUEST_OK', requestId, parameters: new Map() };\n }\n\n // ── Fetch ──\n case D14.FETCH: {\n // Draft-14 §9.16: inline Priority, GroupOrder before FetchType\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const priority = buf[pos++]!;\n const groupOrder = buf[pos++]!;\n const { value: fetchTypeVal, bytesRead: ftBytes } = readVarint(buf, pos);\n pos += ftBytes;\n\n let fetch: Fetch['fetch'];\n if (fetchTypeVal === 0x1n) {\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n const { value: trackName, bytesRead: tnBytes } = readLengthPrefixedBytes(buf, pos);\n pos += tnBytes;\n const { value: startLocation, bytesRead: slBytes } = readLocation(buf, pos);\n pos += slBytes;\n const { value: endLocation, bytesRead: elBytes } = readLocation(buf, pos);\n pos += elBytes;\n fetch = { fetchType: 0x1, trackNamespace, trackName, startLocation, endLocation };\n } else if (fetchTypeVal === 0x2n || fetchTypeVal === 0x3n) {\n const { value: joiningRequestId, bytesRead: jBytes } = readVarint(buf, pos);\n pos += jBytes;\n const { value: joiningStart, bytesRead: jsBytes } = readVarint(buf, pos);\n pos += jsBytes;\n fetch = { fetchType: Number(fetchTypeVal) as 0x2 | 0x3, joiningRequestId, joiningStart };\n } else {\n throw new ProtocolViolationError(`Invalid Fetch Type: 0x${fetchTypeVal.toString(16)}`);\n }\n\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n\n // Normalize inline fields → parameters\n parameters.set(MessageParam.SUBSCRIBER_PRIORITY, [varint(priority)]);\n parameters.set(MessageParam.GROUP_ORDER, [varint(groupOrder)]);\n\n this.assertConsumed(pos, payloadEnd, 'FETCH');\n return { type: 'FETCH', requestId, fetch, parameters };\n }\n\n case D14.FETCH_OK: {\n // Draft-14 §9.17: inline GroupOrder\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const groupOrder = buf[pos++]!;\n const { value: endOfTrack } = readUint8(buf, pos);\n pos += 1;\n const { value: endLocation, bytesRead: elBytes } = readLocation(buf, pos);\n pos += elBytes;\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n\n parameters.set(MessageParam.GROUP_ORDER, [varint(groupOrder)]);\n\n this.assertConsumed(pos, payloadEnd, 'FETCH_OK');\n // No trackExtensions in draft-14\n const trackExtensions: TrackExtensions = new Map();\n return { type: 'FETCH_OK', requestId, endOfTrack, endLocation, parameters, trackExtensions };\n }\n\n case D14.FETCH_CANCEL: {\n const { value: requestId, bytesRead } = readVarint(buf, pos);\n pos += bytesRead;\n this.assertConsumed(pos, payloadEnd, 'FETCH_CANCEL');\n return { type: 'FETCH_CANCEL', requestId };\n }\n\n // ── Namespace ──\n case D14.PUBLISH_NAMESPACE: {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n validateTrackNamespace(trackNamespace);\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n this.assertConsumed(pos, payloadEnd, 'PUBLISH_NAMESPACE');\n return { type: 'PUBLISH_NAMESPACE', requestId, trackNamespace, parameters };\n }\n\n case D14.PUBLISH_NAMESPACE_OK: {\n const { value: requestId, bytesRead } = readVarint(buf, pos);\n pos += bytesRead;\n this.assertConsumed(pos, payloadEnd, 'PUBLISH_NAMESPACE_OK');\n return { type: 'PUBLISH_NAMESPACE_OK', requestId };\n }\n\n case D14.PUBLISH_NAMESPACE_ERROR: {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: errorCode, bytesRead: ecBytes } = readVarint(buf, pos);\n pos += ecBytes;\n const { value: errorReason, bytesRead: erBytes } = readReasonPhrase(buf, pos);\n pos += erBytes;\n this.assertConsumed(pos, payloadEnd, 'PUBLISH_NAMESPACE_ERROR');\n return { type: 'PUBLISH_NAMESPACE_ERROR', requestId, errorCode, errorReason };\n }\n\n case D14.PUBLISH_NAMESPACE_DONE: {\n // Draft-14 §9.26: Track Namespace (tuple), NOT requestId\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n this.assertConsumed(pos, payloadEnd, 'PUBLISH_NAMESPACE_DONE');\n return { type: 'PUBLISH_NAMESPACE_DONE', trackNamespace };\n }\n\n case D14.PUBLISH_NAMESPACE_CANCEL: {\n // Draft-14 §9.27: Track Namespace (tuple), Error Code, Error Reason\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n const { value: errorCode, bytesRead: ecBytes } = readVarint(buf, pos);\n pos += ecBytes;\n const { value: errorReason, bytesRead: erBytes } = readReasonPhrase(buf, pos);\n pos += erBytes;\n this.assertConsumed(pos, payloadEnd, 'PUBLISH_NAMESPACE_CANCEL');\n return { type: 'PUBLISH_NAMESPACE_CANCEL', trackNamespace, errorCode, errorReason };\n }\n\n case D14.SUBSCRIBE_NAMESPACE: {\n // Draft-14 §9.28: no subscribeOptions\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackNamespacePrefix, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n validateTrackNamespacePrefix(trackNamespacePrefix);\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n this.assertConsumed(pos, payloadEnd, 'SUBSCRIBE_NAMESPACE');\n return { type: 'SUBSCRIBE_NAMESPACE', requestId, trackNamespacePrefix, parameters };\n }\n\n case D14.UNSUBSCRIBE_NAMESPACE: {\n const { value: trackNamespacePrefix, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n this.assertConsumed(pos, payloadEnd, 'UNSUBSCRIBE_NAMESPACE');\n return { type: 'UNSUBSCRIBE_NAMESPACE', trackNamespacePrefix };\n }\n\n // ── Track Status ──\n case D14.TRACK_STATUS: {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n const { value: trackName, bytesRead: tnBytes } = readLengthPrefixedBytes(buf, pos);\n pos += tnBytes;\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n this.assertConsumed(pos, payloadEnd, 'TRACK_STATUS');\n return { type: 'TRACK_STATUS', requestId, trackNamespace, trackName, parameters };\n }\n\n // ── Session messages ──\n case D14.PUBLISH_DONE: {\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: statusCode, bytesRead: scBytes } = readVarint(buf, pos);\n pos += scBytes;\n const { value: streamCount, bytesRead: stBytes } = readVarint(buf, pos);\n pos += stBytes;\n const { value: errorReason, bytesRead: erBytes } = readReasonPhrase(buf, pos);\n pos += erBytes;\n this.assertConsumed(pos, payloadEnd, 'PUBLISH_DONE');\n return { type: 'PUBLISH_DONE', requestId, statusCode, streamCount, errorReason };\n }\n\n case D14.GOAWAY: {\n const { value: uriLen, bytesRead: ulBytes } = readVarint(buf, pos);\n pos += ulBytes;\n const uriBytes = buf.slice(pos, pos + Number(uriLen));\n pos += Number(uriLen);\n const newSessionUri = new TextDecoder().decode(uriBytes);\n this.assertConsumed(pos, payloadEnd, 'GOAWAY');\n return { type: 'GOAWAY', newSessionUri };\n }\n\n case D14.MAX_REQUEST_ID: {\n const { value: maxRequestId, bytesRead } = readVarint(buf, pos);\n pos += bytesRead;\n this.assertConsumed(pos, payloadEnd, 'MAX_REQUEST_ID');\n return { type: 'MAX_REQUEST_ID', maxRequestId };\n }\n\n case D14.REQUESTS_BLOCKED: {\n const { value: maximumRequestId, bytesRead } = readVarint(buf, pos);\n pos += bytesRead;\n this.assertConsumed(pos, payloadEnd, 'REQUESTS_BLOCKED');\n return { type: 'REQUESTS_BLOCKED', maximumRequestId };\n }\n\n case D14.UNSUBSCRIBE: {\n const { value: requestId, bytesRead } = readVarint(buf, pos);\n pos += bytesRead;\n this.assertConsumed(pos, payloadEnd, 'UNSUBSCRIBE');\n return { type: 'UNSUBSCRIBE', requestId };\n }\n\n // ── Publish (subscriber receives from publisher) ──\n case D14.PUBLISH: {\n // Draft-14 §9.13: PUBLISH { Request ID, Track Namespace, Track Name,\n // Track Alias, Group Order (8), Content Exists (8), [Largest Location],\n // Forward (8), Parameters }\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: trackNamespace, bytesRead: nsBytes } = readTuple(buf, pos);\n pos += nsBytes;\n const { value: trackName, bytesRead: tnBytes } = readLengthPrefixedBytes(buf, pos);\n pos += tnBytes;\n const { value: trackAlias, bytesRead: taBytes } = readVarint(buf, pos);\n pos += taBytes;\n const pubGroupOrder = buf[pos++]!;\n const contentExists = buf[pos++]!;\n if (contentExists !== 0 && contentExists !== 1) {\n throw new ProtocolViolationError(\n `PUBLISH Content Exists must be 0 or 1, got ${contentExists}`,\n );\n }\n let largestLocation: Location | undefined;\n if (contentExists === 1) {\n const { value, bytesRead: llBytes } = readLocation(buf, pos);\n pos += llBytes;\n largestLocation = value;\n }\n const pubForward = buf[pos++]!;\n const { value: pubParams, bytesRead: ppBytes } = this.readParams(buf, pos);\n pos += ppBytes;\n\n pubParams.set(MessageParam.GROUP_ORDER, [varint(pubGroupOrder)]);\n pubParams.set(MessageParam.FORWARD, [varint(pubForward)]);\n if (largestLocation !== undefined) {\n const encoded = new Uint8Array(locationEncodingLength(largestLocation));\n writeLocation(largestLocation, encoded, 0);\n pubParams.set(MessageParam.LARGEST_OBJECT, [encoded]);\n }\n\n this.assertConsumed(pos, payloadEnd, 'PUBLISH');\n // No trackExtensions in draft-14\n const pubTrackExtensions: TrackExtensions = new Map();\n return {\n type: 'PUBLISH', requestId, trackNamespace, trackName,\n trackAlias, parameters: pubParams, trackExtensions: pubTrackExtensions,\n };\n }\n\n case D14.PUBLISH_ERROR: {\n // Draft-14 §9.15: PUBLISH_ERROR { Request ID, Error Code, Error Reason }\n // Normalized to REQUEST_ERROR\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: errorCode, bytesRead: ecBytes } = readVarint(buf, pos);\n pos += ecBytes;\n const { value: errorReason, bytesRead: erBytes } = readReasonPhrase(buf, pos);\n pos += erBytes;\n this.assertConsumed(pos, payloadEnd, 'PUBLISH_ERROR');\n return {\n type: 'REQUEST_ERROR',\n requestId,\n errorCode,\n retryInterval: varint(0),\n errorReason,\n };\n }\n\n case D14.SUBSCRIBE_UPDATE: {\n // Draft-14 §9.10: SUBSCRIBE_UPDATE → normalized to REQUEST_UPDATE\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n const { value: existingRequestId, bytesRead: sridBytes } = readVarint(buf, pos);\n pos += sridBytes;\n const { value: startLoc, bytesRead: slBytes } = readLocation(buf, pos);\n pos += slBytes;\n const { value: endGroupVal, bytesRead: egBytes } = readVarint(buf, pos);\n pos += egBytes;\n const subPriority = buf[pos++]!;\n const subForward = buf[pos++]!;\n const { value: parameters, bytesRead: pBytes } = this.readParams(buf, pos);\n pos += pBytes;\n\n // Normalize inline fields → parameters\n parameters.set(MessageParam.SUBSCRIBER_PRIORITY, [varint(subPriority)]);\n parameters.set(MessageParam.FORWARD, [varint(subForward)]);\n\n // Draft-14 §9.10: \"End Group: The end Group ID, plus 1.\n // A value of 0 means the subscription is open-ended.\"\n // Normalize wire value → logical: logical = wire - 1 (wire > 0)\n const logicalEndGroup = endGroupVal > 0n ? varint((endGroupVal as bigint) - 1n) : varint(0);\n\n // Encode filter as SUBSCRIPTION_FILTER bytes (draft-16 format)\n // so the session state machine sees a uniform parameter\n if (endGroupVal > 0n) {\n // AbsoluteRange: start + end\n const filterBuf = new Uint8Array(\n varintEncodingLength(varint(0x4)) +\n locationEncodingLength(startLoc) +\n varintEncodingLength(logicalEndGroup),\n );\n let fPos = writeVarint(varint(0x4), filterBuf, 0);\n fPos += writeLocation(startLoc, filterBuf, fPos);\n writeVarint(logicalEndGroup, filterBuf, fPos);\n parameters.set(MessageParam.SUBSCRIPTION_FILTER, [filterBuf]);\n } else if (startLoc.group > 0n || startLoc.object > 0n) {\n // AbsoluteStart: start only\n const filterBuf = new Uint8Array(\n varintEncodingLength(varint(0x3)) +\n locationEncodingLength(startLoc),\n );\n let fPos = writeVarint(varint(0x3), filterBuf, 0);\n writeLocation(startLoc, filterBuf, fPos);\n parameters.set(MessageParam.SUBSCRIPTION_FILTER, [filterBuf]);\n }\n // If start=0,0 and endGroup=0, no filter needed (open-ended)\n\n this.assertConsumed(pos, payloadEnd, 'SUBSCRIBE_UPDATE');\n return { type: 'REQUEST_UPDATE', requestId, existingRequestId, parameters };\n }\n\n // ── PUBLISH_OK (subscriber acknowledges a publisher's PUBLISH) ──\n case D14.PUBLISH_OK: {\n // Draft-14 §9.14: PUBLISH_OK {\n // Request ID (i),\n // Forward (8),\n // Subscriber Priority (8),\n // Group Order (8),\n // Filter Type (i),\n // [Start Location (Location)],\n // [End Group (i)],\n // Number of Parameters (i),\n // Parameters (..) ...\n // }\n // Mirrors encodePublishOk — see there for the wire-format reference.\n const { value: pubOkRequestId, bytesRead: poRidBytes } = readVarint(buf, pos);\n pos += poRidBytes;\n const poForward = buf[pos++]!;\n const poPriority = buf[pos++]!;\n const poGroupOrder = buf[pos++]!;\n const { value: poFilterType, bytesRead: poFtBytes } = readVarint(buf, pos);\n pos += poFtBytes;\n\n let poStartLoc: Location | undefined;\n let poEndGroup: Varint | undefined;\n if (poFilterType === 0x3n || poFilterType === 0x4n) {\n const { value: sl, bytesRead: slBytes } = readLocation(buf, pos);\n pos += slBytes;\n poStartLoc = sl;\n }\n if (poFilterType === 0x4n) {\n const { value: eg, bytesRead: egBytes } = readVarint(buf, pos);\n pos += egBytes;\n poEndGroup = eg;\n }\n\n const { value: poParams, bytesRead: poPBytes } = this.readParams(buf, pos);\n pos += poPBytes;\n\n // Normalize inline fields → parameters (same shape as SUBSCRIBE_OK\n // / FETCH_OK produce on decode).\n poParams.set(MessageParam.FORWARD, [varint(poForward)]);\n poParams.set(MessageParam.SUBSCRIBER_PRIORITY, [varint(poPriority)]);\n poParams.set(MessageParam.GROUP_ORDER, [varint(poGroupOrder)]);\n const poFilterBytes = this.encodeFilterBytes(varint(poFilterType), poStartLoc, poEndGroup);\n poParams.set(MessageParam.SUBSCRIPTION_FILTER, [poFilterBytes]);\n\n this.assertConsumed(pos, payloadEnd, 'PUBLISH_OK');\n return { type: 'PUBLISH_OK', requestId: pubOkRequestId, parameters: poParams };\n }\n\n default:\n throw new ProtocolViolationError(`Unknown draft-14 message type: 0x${typeNum.toString(16)}`);\n }\n }\n\n // ─── Utility ──────────────────────────────────────────────────────\n\n private readParams(buf: Uint8Array, offset: number): { value: Parameters; bytesRead: number } {\n let pos = offset;\n const { value: count, bytesRead: countBytes } = readVarint(buf, pos);\n pos += countBytes;\n const { value, bytesRead: kvpBytes } = readKvpListAbsolute(buf, pos, Number(count));\n pos += kvpBytes;\n return { value, bytesRead: pos - offset };\n }\n\n private assertConsumed(pos: number, payloadEnd: number, msgName: string): void {\n if (pos !== payloadEnd) {\n throw new RangeError(\n `Payload not fully consumed for ${msgName}: consumed ${pos} bytes, expected ${payloadEnd}`,\n );\n }\n }\n\n /** Extract a varint value from params, or return undefined if absent. */\n private extractParamVarint(params: Parameters, key: Varint): Varint | undefined {\n const values = params.get(key);\n if (!values || values.length === 0) return undefined;\n return values[0] as Varint;\n }\n\n /** Extract exactly one locally-authored varint used by an inline field. */\n private extractSingleVarintParam(\n params: Parameters,\n key: Varint,\n field: string,\n message: string,\n ): Varint | undefined {\n const values = params.get(key);\n if (values === undefined) return undefined;\n if (values.length !== 1 || typeof values[0] !== 'bigint') {\n throw new ProtocolViolationError(\n `${message}: ${field} must contain exactly one integer`,\n );\n }\n return values[0] as Varint;\n }\n\n /**\n * Encode filter fields as SUBSCRIPTION_FILTER bytes (draft-16 wire format).\n * FilterType (i), [StartGroup (i), StartObject (i)], [EndGroup (i)]\n */\n private encodeFilterBytes(\n filterType: Varint,\n startLocation?: Location,\n endGroup?: Varint,\n ): Uint8Array {\n let size = varintEncodingLength(filterType);\n if (startLocation) {\n size += locationEncodingLength(startLocation);\n }\n if (endGroup !== undefined) {\n size += varintEncodingLength(endGroup);\n }\n const buf = new Uint8Array(size);\n let off = writeVarint(filterType, buf, 0);\n if (startLocation) {\n off += writeLocation(startLocation, buf, off);\n }\n if (endGroup !== undefined) {\n writeVarint(endGroup, buf, off);\n }\n return buf;\n }\n\n /** Clone a parameters map, removing specified keys. */\n private cloneParamsWithout(params: Parameters, keysToRemove: Varint[]): Parameters {\n const result: Parameters = new Map();\n const removeSet = new Set(keysToRemove.map(k => k as bigint));\n for (const [key, values] of params) {\n if (!removeSet.has(key)) {\n result.set(key, values);\n }\n }\n return result;\n }\n\n /**\n * Extract and parse SUBSCRIPTION_FILTER from parameters.\n *\n * In draft-16, SUBSCRIPTION_FILTER (type 0x21, odd = bytes) carries:\n * FilterType (i), [StartGroup (i), StartObject (i)], [EndGroup (i)]\n *\n * Returns parsed filter fields for inline encoding in draft-14 wire format.\n *\n * @see draft-ietf-moq-transport-16 §5.1.2, §9.2.2.5\n * @see draft-ietf-moq-transport-14 §9.7 (inline filter fields)\n */\n private extractFilter(params: Parameters): {\n filterType: Varint;\n startLocation?: Location;\n endGroup?: Varint;\n } {\n const values = params.get(MessageParam.SUBSCRIPTION_FILTER);\n if (!values || values.length === 0) {\n // Default: NextGroupStart (0x1) — no Start/End fields\n return { filterType: varint(0x1) };\n }\n\n const val = values[0]!;\n if (typeof val === 'bigint') {\n // Legacy: raw varint filter type (no location data)\n return { filterType: val as Varint };\n }\n\n // Parse bytes: FilterType (i), [StartGroup (i), StartObject (i)], [EndGroup (i)]\n const bytes = val as Uint8Array;\n let pos = 0;\n const { value: filterType, bytesRead: ftBytes } = readVarint(bytes, pos);\n pos += ftBytes;\n\n let startLocation: Location | undefined;\n let endGroup: Varint | undefined;\n\n if (filterType === 0x3n || filterType === 0x4n) {\n // AbsoluteStart or AbsoluteRange: read Start Location\n const { value: startLoc, bytesRead: slBytes } = readLocation(bytes, pos);\n pos += slBytes;\n startLocation = startLoc;\n }\n if (filterType === 0x4n) {\n // AbsoluteRange: read End Group\n const { value: eg, bytesRead: egBytes } = readVarint(bytes, pos);\n pos += egBytes;\n endGroup = eg;\n }\n\n const result: { filterType: Varint; startLocation?: Location; endGroup?: Varint } = {\n filterType: varint(filterType),\n };\n if (startLocation !== undefined) result.startLocation = startLocation;\n if (endGroup !== undefined) result.endGroup = endGroup;\n return result;\n }\n\n /**\n * Encode REQUEST_UPDATE as draft-14 SUBSCRIBE_UPDATE (type 0x2).\n *\n * Draft-14 §9.10:\n * SUBSCRIBE_UPDATE {\n * Type (i) = 0x2, Length (16),\n * Request ID (i), Subscription Request ID (i),\n * Start Location (Location), End Group (i),\n * Subscriber Priority (8), Forward (8),\n * Number of Parameters (i), Parameters (..) ...\n * }\n *\n * @see draft-ietf-moq-transport-14 §9.10\n */\n private encodeSubscribeUpdate(msg: RequestUpdate): Uint8Array {\n const priority = Number(this.extractParamVarint(msg.parameters, MessageParam.SUBSCRIBER_PRIORITY) ?? 128n);\n const forward = Number(this.extractParamVarint(msg.parameters, MessageParam.FORWARD) ?? 1n);\n this.assertUint8(priority, 'SUBSCRIBER_PRIORITY', 'SUBSCRIBE_UPDATE');\n this.assertUint8(forward, 'FORWARD', 'SUBSCRIBE_UPDATE');\n const filter = this.extractFilter(msg.parameters);\n\n // Draft-14 §9.10: Start Location is always present (default to 0,0 if not specified)\n const startLocation: Location = filter.startLocation ?? { group: varint(0), object: varint(0) };\n // Draft-14 §9.10: \"End Group: The end Group ID, plus 1.\n // A value of 0 means the subscription is open-ended.\"\n // Wire value = logical + 1 for non-zero, 0 for open-ended.\n const logicalEndGroup = filter.endGroup ?? varint(0);\n const wireEndGroup: Varint = logicalEndGroup === 0n\n ? varint(0)\n : varint((logicalEndGroup as bigint) + 1n);\n\n const remainingParams = this.cloneParamsWithout(msg.parameters, [\n MessageParam.SUBSCRIBER_PRIORITY,\n MessageParam.FORWARD,\n MessageParam.SUBSCRIPTION_FILTER,\n ]);\n\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n varintEncodingLength(msg.existingRequestId) +\n locationEncodingLength(startLocation) +\n varintEncodingLength(wireEndGroup) +\n 1 + // Subscriber Priority (8)\n 1 + // Forward (8)\n this.paramsLength(remainingParams);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeVarint(msg.existingRequestId, payload, pos);\n pos += writeLocation(startLocation, payload, pos);\n pos += writeVarint(wireEndGroup, payload, pos);\n payload[pos++] = priority & 0xff;\n payload[pos++] = forward & 0xff;\n pos += this.writeParams(remainingParams, payload, pos);\n\n return this.frame16(D14.SUBSCRIBE_UPDATE, payload);\n }\n\n /**\n * Frame with varint length (for PUBLISH, PUBLISH_OK, PUBLISH_ERROR).\n * @see draft-ietf-moq-transport-14 §9.13, §9.14, §9.15\n */\n private frameVarint(typeCode: number, payload: Uint8Array): Uint8Array {\n const typeVarint = varint(typeCode);\n const typeLen = varintEncodingLength(typeVarint);\n const payloadLenVarint = varint(payload.length);\n const payloadLenLen = varintEncodingLength(payloadLenVarint);\n const total = typeLen + payloadLenLen + payload.length;\n const buf = new Uint8Array(total);\n let pos = 0;\n pos += writeVarint(typeVarint, buf, pos);\n pos += writeVarint(payloadLenVarint, buf, pos);\n buf.set(payload, pos);\n return buf;\n }\n\n /**\n * Encode PUBLISH_OK.\n *\n * Draft-14 §9.14:\n * PUBLISH_OK {\n * Type (i) = 0x1E,\n * Length (i),\n * Request ID (i),\n * Forward (8),\n * Subscriber Priority (8),\n * Group Order (8),\n * Filter Type (i),\n * [Start Location (Location)],\n * [End Group (i)],\n * Number of Parameters (i),\n * Parameters (..) ...\n * }\n *\n * @see draft-ietf-moq-transport-14 §9.14\n */\n private encodePublishOk(msg: PublishOk): Uint8Array {\n const forward = Number(this.extractSingleVarintParam(\n msg.parameters, MessageParam.FORWARD, 'FORWARD', 'PUBLISH_OK',\n ) ?? 1n);\n const subscriberPriority = Number(this.extractSingleVarintParam(\n msg.parameters, MessageParam.SUBSCRIBER_PRIORITY, 'SUBSCRIBER_PRIORITY', 'PUBLISH_OK',\n ) ?? 128n);\n // Draft-14 §9.14: GROUP_ORDER must be 0x1 (Ascending) or 0x2 (Descending); 0x0 is a protocol error.\n const groupOrder = Number(this.extractSingleVarintParam(\n msg.parameters, MessageParam.GROUP_ORDER, 'GROUP_ORDER', 'PUBLISH_OK',\n ) ?? 1n);\n this.assertUint8(forward, 'FORWARD', 'PUBLISH_OK');\n this.assertUint8(subscriberPriority, 'SUBSCRIBER_PRIORITY', 'PUBLISH_OK');\n this.assertUint8(groupOrder, 'GROUP_ORDER', 'PUBLISH_OK');\n if (forward !== 0 && forward !== 1) {\n throw new ProtocolViolationError(`PUBLISH_OK: FORWARD must be 0 or 1, got ${forward}`);\n }\n if (groupOrder !== 1 && groupOrder !== 2) {\n throw new ProtocolViolationError(\n `PUBLISH_OK: GROUP_ORDER must be Ascending (0x1) or Descending (0x2), got ${groupOrder}`,\n );\n }\n\n // Extract filter from parameters or default to NextGroupStart (0x1)\n const filterResult = this.extractFilter(msg.parameters);\n const filterType = filterResult.filterType;\n\n // Build remaining params (exclude inline fields)\n const remainingParams = new Map(msg.parameters);\n remainingParams.delete(varint(MessageParam.FORWARD));\n remainingParams.delete(varint(MessageParam.SUBSCRIBER_PRIORITY));\n remainingParams.delete(varint(MessageParam.GROUP_ORDER));\n remainingParams.delete(varint(MessageParam.SUBSCRIPTION_FILTER));\n\n // Calculate filter-dependent fields length\n let filterFieldsLen = varintEncodingLength(filterType); // Filter Type\n if (filterResult.startLocation) {\n filterFieldsLen += locationEncodingLength(filterResult.startLocation);\n }\n if (filterResult.endGroup !== undefined) {\n filterFieldsLen += varintEncodingLength(filterResult.endGroup);\n }\n\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n 1 + // Forward (8)\n 1 + // Subscriber Priority (8)\n 1 + // Group Order (8)\n filterFieldsLen +\n this.paramsLength(remainingParams);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n payload[pos++] = forward & 0xff;\n payload[pos++] = subscriberPriority & 0xff;\n payload[pos++] = groupOrder & 0xff;\n pos += writeVarint(filterType, payload, pos);\n if (filterResult.startLocation) {\n pos += writeLocation(filterResult.startLocation, payload, pos);\n }\n if (filterResult.endGroup !== undefined) {\n pos += writeVarint(filterResult.endGroup, payload, pos);\n }\n pos += this.writeParams(remainingParams, payload, pos);\n\n return this.frameVarint(D14.PUBLISH_OK, payload);\n }\n\n /**\n * Encode PUBLISH_ERROR.\n *\n * Draft-14 §9.15:\n * PUBLISH_ERROR {\n * Type (i) = 0x1F,\n * Length (i),\n * Request ID (i),\n * Error Code (i),\n * Error Reason (Reason Phrase)\n * }\n *\n * @see draft-ietf-moq-transport-14 §9.15\n */\n private encodePublishError(msg: PublishError): Uint8Array {\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n varintEncodingLength(msg.errorCode) +\n reasonPhraseEncodingLength(msg.errorReason);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeVarint(msg.errorCode, payload, pos);\n pos += writeReasonPhrase(msg.errorReason, payload, pos);\n\n return this.frameVarint(D14.PUBLISH_ERROR, payload);\n }\n\n /**\n * Encode SUBSCRIBE_OK.\n *\n * Draft-14 §9.8:\n * SUBSCRIBE_OK {\n * Type (i) = 0x04,\n * Length (16),\n * Request ID (i),\n * Track Alias (i),\n * Expires (i),\n * Group Order (8),\n * Content Exists (8),\n * [Largest Location],\n * Number of Parameters (i),\n * Parameters (..) ...\n * }\n *\n * @see draft-ietf-moq-transport-14 §9.8\n */\n private encodeSubscribeOk(msg: SubscribeOk): Uint8Array {\n // Extract inline fields from normalized parameters\n const groupOrder = Number(msg.parameters.get(MessageParam.GROUP_ORDER)?.[0] ?? 1n);\n const expiresRaw = msg.parameters.get(MessageParam.EXPIRES)?.[0];\n const expires = typeof expiresRaw === 'bigint' ? varint(expiresRaw) : varint(0n);\n const largestObjectBytes = msg.parameters.get(MessageParam.LARGEST_OBJECT)?.[0];\n const contentExists = largestObjectBytes !== undefined ? 1 : 0;\n\n // Build remaining params (strip inline fields)\n const remainingParams: Parameters = new Map();\n for (const [key, values] of msg.parameters) {\n if (\n key === MessageParam.GROUP_ORDER ||\n key === MessageParam.EXPIRES ||\n key === MessageParam.LARGEST_OBJECT\n ) continue;\n remainingParams.set(key, values);\n }\n\n // Calculate payload size\n let payloadLen =\n varintEncodingLength(msg.requestId) +\n varintEncodingLength(msg.trackAlias) +\n varintEncodingLength(expires) +\n 1 + // Group Order (8)\n 1; // Content Exists (8)\n\n if (contentExists === 1 && largestObjectBytes instanceof Uint8Array) {\n payloadLen += largestObjectBytes.byteLength;\n }\n\n // Params size: count varint + each param\n const paramsCountLen = varintEncodingLength(varint(BigInt(remainingParams.size)));\n let paramsTotalLen = paramsCountLen;\n for (const [key, values] of remainingParams) {\n for (const val of values) {\n paramsTotalLen += varintEncodingLength(key);\n if (typeof val === 'bigint') {\n paramsTotalLen += varintEncodingLength(varint(varintEncodingLength(val)));\n paramsTotalLen += varintEncodingLength(val);\n } else if (val instanceof Uint8Array) {\n paramsTotalLen += varintEncodingLength(varint(BigInt(val.byteLength)));\n paramsTotalLen += val.byteLength;\n } else {\n throw new ProtocolViolationError(\n 'Location-valued parameter cannot be encoded as draft-14 KVP',\n );\n }\n }\n }\n payloadLen += paramsTotalLen;\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeVarint(msg.trackAlias, payload, pos);\n pos += writeVarint(expires, payload, pos);\n payload[pos++] = groupOrder & 0xFF;\n payload[pos++] = contentExists;\n\n if (contentExists === 1 && largestObjectBytes instanceof Uint8Array) {\n payload.set(largestObjectBytes, pos);\n pos += largestObjectBytes.byteLength;\n }\n\n pos += this.writeParams(remainingParams, payload, pos);\n\n return this.frame16(D14.SUBSCRIBE_OK, payload);\n }\n\n /**\n * Encode PUBLISH_DONE.\n *\n * Draft-14 §9.11:\n * PUBLISH_DONE {\n * Type (i) = 0x0B,\n * Length (16),\n * Request ID (i),\n * Status Code (i),\n * Stream Count (i),\n * Error Reason (Reason Phrase)\n * }\n *\n * @see draft-ietf-moq-transport-14 §9.11\n */\n private encodePublishDone(msg: PublishDone): Uint8Array {\n const payloadLen =\n varintEncodingLength(msg.requestId) +\n varintEncodingLength(msg.statusCode) +\n varintEncodingLength(msg.streamCount) +\n reasonPhraseEncodingLength(msg.errorReason);\n\n const payload = new Uint8Array(payloadLen);\n let pos = 0;\n pos += writeVarint(msg.requestId, payload, pos);\n pos += writeVarint(msg.statusCode, payload, pos);\n pos += writeVarint(msg.streamCount, payload, pos);\n pos += writeReasonPhrase(msg.errorReason, payload, pos);\n\n return this.frame16(D14.PUBLISH_DONE, payload);\n }\n}\n","/**\n * Draft-18 control message codec.\n *\n * A pure wire codec for the draft-18 control plane: SETUP, the request/response\n * messages (SUBSCRIBE/SUBSCRIBE_OK, FETCH/FETCH_OK, PUBLISH, TRACK_STATUS,\n * PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE, SUBSCRIBE_TRACKS, REQUEST_OK,\n * REQUEST_ERROR, REQUEST_UPDATE), the continuation messages (NAMESPACE,\n * NAMESPACE_DONE, PUBLISH_BLOCKED), and PUBLISH_DONE. Stream topology and request\n * correlation live in the I/O layer; truly unhandled message types throw an\n * explicit \"not implemented\" rather than falling back.\n *\n * Framing is `Type (vi64) + Length (16) + Payload` (the Length is a 16-bit field,\n * as in draft-16; the Type and the payload's integer fields use vi64). Response\n * messages OMIT the Request ID — SUBSCRIBE_OK/REQUEST_OK/REQUEST_ERROR/FETCH_OK\n * decode with `requestId === undefined`, to be stamped by the topology from stream\n * context (never a placeholder).\n *\n * Supported semantics:\n * - Track Properties (§2.5) on PUBLISH / SUBSCRIBE_OK / FETCH_OK and a\n * TRACK_STATUS_OK (REQUEST_OK) — see `track-properties-18.ts`. On a REQUEST_OK\n * the codec decodes them context-free; the session enforces which contexts\n * may carry them.\n * - REQUEST_ERROR optional Redirect (§10.6.2), error code REDIRECT (0x34).\n * - vi64 fields are full uint64 (Request ID, Track Alias, Locations, Redirect\n * lengths, Track Property values). Non-Location `varint` message parameters\n * are mapped through the QUIC `Varint` (≤ 2^62-1) at the current boundary;\n * LARGEST_OBJECT (a Location) is full uint64.\n *\n * @see draft-ietf-moq-transport-18 §10 (control messages), §2.5 (Properties)\n * @module\n */\n\nimport type { ControlCodec, DecodedControlMessage } from './codec.js';\nimport type {\n ControlMessage,\n RequestId,\n Parameters,\n ParameterValue,\n Subscribe,\n SubscribeOk,\n RequestOk,\n RequestErrorMsg,\n RequestUpdate,\n Fetch,\n FetchOk,\n StandaloneFetch,\n JoiningFetch,\n TrackStatus,\n PublishNamespace,\n Publish,\n PublishDone,\n SubscribeNamespace,\n PublishBlocked,\n Namespace,\n NamespaceDone,\n Goaway,\n SetupOptionMap,\n SetupOptionValue,\n TrackProperties,\n} from './messages.js';\nimport type { Location } from '../primitives/location.js';\nimport { readVi64, writeVi64, vi64EncodingLength, MAX_VI64 } from '../primitives/vi64.js';\nimport { ControlMessageType18, SetupOption18 } from './codes-18.js';\nimport { MessageParam } from './parameters.js';\nimport {\n encodeMessageParams18,\n decodeMessageParams18,\n messageParams18EncodingLength,\n DEFAULT_MESSAGE_PARAM_REGISTRY,\n DRAFT22_MESSAGE_PARAM_REGISTRY,\n type MessageParams18,\n type MessageParamRegistry,\n type MessageParamValue,\n} from './message-params-18.js';\nimport { encodeFetchLocationFilter, decodeFetchLocationFilter } from './subscription-filter.js';\nimport {\n encodeTrackProperties18,\n decodeTrackProperties18,\n trackProperties18EncodingLength,\n} from './track-properties-18.js';\nimport { ProtocolViolationError } from '../errors.js';\nimport { validateTrackNamespacePrefix, validateFullTrackName, validateTrackNamespace } from '../primitives/bytes.js';\nimport type { RequestStreamDraft } from '../versions.js';\n\n/** Error Code value that carries an optional Redirect structure (§10.6.2). */\nconst REDIRECT_ERROR_CODE = 0x34n;\n\n/** Read a message's Track Properties, accepting the canonical `trackProperties`\n * or the deprecated `trackExtensions` alias (empty if neither). */\nfunction trackPropsOf(\n msg: { readonly trackProperties?: TrackProperties; readonly trackExtensions?: TrackProperties },\n): TrackProperties {\n return msg.trackProperties ?? msg.trackExtensions ?? new Map();\n}\n\n// ─── vi64 field helpers (length-prefixed bytes, namespace tuple) ─────\n\nfunction vi64BytesLength(bytes: Uint8Array): number {\n return vi64EncodingLength(BigInt(bytes.length)) + bytes.length;\n}\nfunction writeVi64Bytes(bytes: Uint8Array, buf: Uint8Array, offset: number): number {\n let p = offset + writeVi64(BigInt(bytes.length), buf, offset);\n buf.set(bytes, p);\n return p + bytes.length - offset;\n}\nfunction readVi64Bytes(buf: Uint8Array, offset: number): { value: Uint8Array; bytesRead: number } {\n const { value: len, bytesRead } = readVi64(buf, offset);\n const start = offset + bytesRead;\n const n = Number(len);\n if (start + n > buf.length) throw new ProtocolViolationError('vi64 length-prefixed bytes exceed buffer');\n return { value: buf.slice(start, start + n), bytesRead: bytesRead + n };\n}\n\nfunction vi64TupleLength(fields: Uint8Array[]): number {\n let len = vi64EncodingLength(BigInt(fields.length));\n for (const f of fields) len += vi64BytesLength(f);\n return len;\n}\nfunction writeVi64Tuple(fields: Uint8Array[], buf: Uint8Array, offset: number): number {\n let p = offset + writeVi64(BigInt(fields.length), buf, offset);\n for (const f of fields) p += writeVi64Bytes(f, buf, p);\n return p - offset;\n}\nfunction readVi64Tuple(buf: Uint8Array, offset: number): { value: Uint8Array[]; bytesRead: number } {\n let p = offset;\n const { value: count, bytesRead } = readVi64(buf, p);\n p += bytesRead;\n const fields: Uint8Array[] = [];\n for (let i = 0n; i < count; i++) {\n const r = readVi64Bytes(buf, p);\n fields.push(r.value);\n p += r.bytesRead;\n }\n return { value: fields, bytesRead: p - offset };\n}\n/** Encode a namespace tuple into a fresh buffer (used by NAMESPACE / NAMESPACE_DONE). */\nfunction encodeTuple(fields: Uint8Array[]): Uint8Array {\n const buf = new Uint8Array(vi64TupleLength(fields));\n writeVi64Tuple(fields, buf, 0);\n return buf;\n}\n\n// ─── Location (two vi64: group, object — full uint64, no QUIC cap) ──────\n\nfunction vi64LocationLength(loc: Location): number {\n return vi64EncodingLength(loc.group) + vi64EncodingLength(loc.object);\n}\nfunction writeVi64Location(loc: Location, buf: Uint8Array, offset: number): number {\n let p = offset + writeVi64(loc.group, buf, offset);\n p += writeVi64(loc.object, buf, p);\n return p - offset;\n}\nfunction readVi64Location(buf: Uint8Array, offset: number): { value: Location; bytesRead: number } {\n const g = readVi64(buf, offset);\n const o = readVi64(buf, offset + g.bytesRead);\n return { value: { group: g.value, object: o.value }, bytesRead: g.bytesRead + o.bytesRead };\n}\n\n// ─── reason phrase (vi64 length-prefixed UTF-8) ─────────────────────\n\nconst TEXT_ENCODER = new TextEncoder();\nconst TEXT_DECODER = new TextDecoder();\n\n/** §10.4: maximum New Session URI length; longer → PROTOCOL_VIOLATION. */\nconst GOAWAY_MAX_URI_LENGTH = 8192;\n\n/** draft-22 LOCATION_FILTER (§9.20.9), the 0x21 message parameter. */\nconst LOCATION_FILTER_TYPE = 0x21n;\n\n/** §1.4.2: maximum Reason Phrase length in bytes; longer → PROTOCOL_VIOLATION. */\nconst REASON_PHRASE_MAX_LENGTH = 1024;\n\nfunction reasonPhrase18Length(s: string): number {\n const bytes = TEXT_ENCODER.encode(s);\n if (bytes.length > REASON_PHRASE_MAX_LENGTH) {\n throw new ProtocolViolationError(`Reason Phrase length ${bytes.length} exceeds maximum ${REASON_PHRASE_MAX_LENGTH} bytes`);\n }\n return vi64BytesLength(bytes);\n}\nfunction writeReasonPhrase18(s: string, buf: Uint8Array, offset: number): number {\n const bytes = TEXT_ENCODER.encode(s);\n if (bytes.length > REASON_PHRASE_MAX_LENGTH) {\n throw new ProtocolViolationError(`Reason Phrase length ${bytes.length} exceeds maximum ${REASON_PHRASE_MAX_LENGTH} bytes`);\n }\n return writeVi64Bytes(bytes, buf, offset);\n}\nfunction readReasonPhrase18(buf: Uint8Array, offset: number): { value: string; bytesRead: number } {\n const { value, bytesRead } = readVi64Bytes(buf, offset);\n if (value.length > REASON_PHRASE_MAX_LENGTH) {\n throw new ProtocolViolationError(`Reason Phrase length ${value.length} exceeds maximum ${REASON_PHRASE_MAX_LENGTH} bytes`);\n }\n return { value: TEXT_DECODER.decode(value), bytesRead };\n}\n\n// ─── Setup Options (KVP, vi64, span the whole payload, no count) ─────\n//\n// Unlike Message Parameters, Setup Options are self-describing via Type parity\n// (even → vi64 value, odd → length-prefixed bytes), so no registry is needed and\n// unknown options decode fine — the session decides which to honor/ignore. Types\n// are delta-encoded and ascending; the block spans the payload. Even-Type values\n// are full-uint64 vi64 (NOT QUIC-Varint-capped).\n\n/** Known singleton Setup Options — MUST NOT be repeated. AUTHORIZATION_TOKEN\n * (0x03) may repeat; unknown Option Types may repeat. */\nconst SETUP_SINGLETONS = new Set<bigint>([\n BigInt(SetupOption18.PATH),\n BigInt(SetupOption18.AUTHORITY),\n BigInt(SetupOption18.MAX_AUTH_TOKEN_CACHE_SIZE),\n BigInt(SetupOption18.MOQT_IMPLEMENTATION),\n]);\n\nfunction setupValueLength(type: bigint, value: SetupOptionValue): number {\n if (type % 2n === 0n) {\n if (typeof value !== 'bigint') {\n throw new ProtocolViolationError(`Setup Option 0x${type.toString(16)} (even) requires a vi64 value`);\n }\n return vi64EncodingLength(value); // full uint64 — not QUIC-range capped\n }\n if (!(value instanceof Uint8Array)) {\n throw new ProtocolViolationError(`Setup Option 0x${type.toString(16)} (odd) requires a byte value`);\n }\n return vi64BytesLength(value);\n}\n\nfunction writeSetupValue(type: bigint, value: SetupOptionValue, buf: Uint8Array, offset: number): number {\n return type % 2n === 0n\n ? writeVi64(value as bigint, buf, offset)\n : writeVi64Bytes(value as Uint8Array, buf, offset);\n}\n\nfunction encodeSetupOptionMap18(options: SetupOptionMap): Uint8Array {\n const entries = [...options.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n const flat: { delta: bigint; type: bigint; value: SetupOptionValue }[] = [];\n let prev = 0n;\n let len = 0;\n for (const [type, values] of entries) {\n // Sender-side duplicate validation: known singletons must not repeat.\n if (SETUP_SINGLETONS.has(type) && values.length > 1) {\n throw new ProtocolViolationError(`Setup Option 0x${type.toString(16)} must not be repeated`);\n }\n for (const value of values) {\n const delta = type - prev;\n prev = type;\n flat.push({ delta, type, value });\n len += vi64EncodingLength(delta) + setupValueLength(type, value);\n }\n }\n const buf = new Uint8Array(len);\n let p = 0;\n for (const { delta, type, value } of flat) {\n p += writeVi64(delta, buf, p);\n p += writeSetupValue(type, value, buf, p);\n }\n return buf;\n}\n\nfunction decodeSetupOptionMap18(payload: Uint8Array): SetupOptionMap {\n const out: SetupOptionMap = new Map();\n let p = 0;\n let prev = 0n;\n while (p < payload.length) {\n const dt = readVi64(payload, p);\n p += dt.bytesRead;\n const type = prev + dt.value;\n if (type > MAX_VI64) throw new ProtocolViolationError(`Setup Option type ${type} exceeds 2^64-1`);\n prev = type;\n let value: SetupOptionValue;\n if (type % 2n === 0n) {\n const v = readVi64(payload, p);\n p += v.bytesRead;\n value = v.value; // full-uint64 vi64 (no Varint cap)\n } else {\n const b = readVi64Bytes(payload, p);\n p += b.bytesRead;\n value = b.value;\n }\n const arr = out.get(type);\n if (arr) arr.push(value);\n else out.set(type, [value]);\n }\n return out;\n}\n\n// ─── parameter bridge (semantic Parameters ↔ typed MessageParams18) ──\n\nfunction isLocation(v: ParameterValue): v is Location {\n return typeof v === 'object' && v !== null && !(v instanceof Uint8Array) && !Array.isArray(v);\n}\n\nfunction toTypedParam(type: bigint, v: ParameterValue, registry: MessageParamRegistry): MessageParamValue {\n const kind = registry.get(type);\n if (kind === undefined) {\n throw new ProtocolViolationError(`Unknown draft-18 message parameter 0x${type.toString(16)}`);\n }\n switch (kind) {\n case 'uint8':\n if (typeof v !== 'bigint') throw new ProtocolViolationError(`Parameter 0x${type.toString(16)} expects uint8`);\n // uint8 wire field is a single byte; reject out-of-range rather than masking.\n if (v < 0n || v > 255n) {\n throw new ProtocolViolationError(`Parameter 0x${type.toString(16)} uint8 value out of range (0..255): ${v}`);\n }\n return { kind: 'uint8', value: Number(v) };\n case 'varint':\n if (typeof v !== 'bigint') throw new ProtocolViolationError(`Parameter 0x${type.toString(16)} expects varint`);\n // draft-18 message-parameter varints are vi64 (full uint64), NOT the QUIC\n // Varint range — only reject negative / above 2^64-1.\n if (v < 0n || v > MAX_VI64) {\n throw new ProtocolViolationError(`Parameter 0x${type.toString(16)} varint value out of range (0..2^64-1): ${v}`);\n }\n return { kind: 'varint', value: v };\n case 'bytes':\n if (!(v instanceof Uint8Array)) throw new ProtocolViolationError(`Parameter 0x${type.toString(16)} expects bytes`);\n return { kind: 'bytes', value: v };\n case 'locationFilter':\n if (!(v instanceof Uint8Array)) throw new ProtocolViolationError(`Parameter 0x${type.toString(16)} expects a LOCATION_FILTER`);\n return { kind: 'locationFilter', value: v };\n case 'location':\n if (!isLocation(v)) throw new ProtocolViolationError(`Parameter 0x${type.toString(16)} expects Location`);\n return { kind: 'location', group: v.group, object: v.object };\n case 'namespace': {\n if (!Array.isArray(v)) throw new ProtocolViolationError(`Parameter 0x${type.toString(16)} expects a Track Namespace tuple`);\n const fields = v as readonly unknown[];\n if (!fields.every((e) => e instanceof Uint8Array)) {\n throw new ProtocolViolationError(`Parameter 0x${type.toString(16)} Track Namespace tuple has a non-Uint8Array field`);\n }\n return { kind: 'namespace', value: [...(fields as readonly Uint8Array[])] };\n }\n }\n}\n\nfunction paramsToTyped(params: Parameters, registry: MessageParamRegistry): MessageParams18 {\n const out = new Map<bigint, MessageParamValue[]>();\n for (const [type, values] of params) {\n if (type === MessageParam.FORWARD) {\n const value = values[0];\n if (values.length !== 1 || typeof value !== 'bigint'\n || (value !== 0n && value !== 1n)) {\n throw new ProtocolViolationError('FORWARD must be exactly one value, 0 or 1');\n }\n }\n out.set(type, values.map((v) => toTypedParam(type, v, registry)));\n }\n return out;\n}\n\nfunction fromTypedParam(v: MessageParamValue): ParameterValue {\n switch (v.kind) {\n case 'uint8':\n return BigInt(v.value);\n case 'varint':\n // draft-18 varint params are vi64 (full uint64); return the raw bigint\n // rather than re-capping through the QUIC Varint range.\n return v.value;\n case 'bytes':\n case 'locationFilter':\n return v.value;\n case 'location':\n return { group: v.group, object: v.object };\n case 'namespace':\n return v.value; // Uint8Array[] — a Track Namespace tuple (NamespaceTuple)\n }\n}\n\nfunction typedToParams(typed: MessageParams18): Parameters {\n const out: Parameters = new Map();\n for (const [type, values] of typed) out.set(type, values.map(fromTypedParam));\n return out;\n}\n\n// ─── codec ───────────────────────────────────────────────────────────\n\nconst notImplemented = (what: string): never => {\n throw new ProtocolViolationError(`Draft18Codec: ${what} is not yet implemented`);\n};\n\nexport class Draft18Codec implements ControlCodec {\n /**\n * @param version 18, or 22 for the draft-22 variant of the same stream\n * model (its differing messages branch on this).\n */\n constructor(readonly version: RequestStreamDraft = 18) {}\n\n /** The message-parameter table of this codec's draft. */\n private get paramRegistry(): MessageParamRegistry {\n return this.version === 22 ? DRAFT22_MESSAGE_PARAM_REGISTRY : DEFAULT_MESSAGE_PARAM_REGISTRY;\n }\n\n encode(msg: ControlMessage): Uint8Array {\n switch (msg.type) {\n case 'SETUP':\n return this.frame(ControlMessageType18.SETUP, encodeSetupOptionMap18(msg.setupOptions));\n case 'SUBSCRIBE':\n return this.frame(ControlMessageType18.SUBSCRIBE, this.encodeSubscribe(msg));\n case 'REQUEST_UPDATE':\n return this.frame(ControlMessageType18.REQUEST_UPDATE, this.encodeRequestUpdate(msg));\n case 'SUBSCRIBE_OK':\n return this.frame(ControlMessageType18.SUBSCRIBE_OK, this.encodeSubscribeOk(msg));\n case 'REQUEST_OK':\n return this.frame(ControlMessageType18.REQUEST_OK, this.encodeRequestOk(msg));\n case 'REQUEST_ERROR':\n return this.frame(ControlMessageType18.REQUEST_ERROR, this.encodeRequestError(msg));\n case 'FETCH':\n return this.frame(ControlMessageType18.FETCH, this.version === 22 ? this.encodeFetch22(msg) : this.encodeFetch(msg));\n case 'FETCH_OK':\n return this.frame(ControlMessageType18.FETCH_OK, this.encodeFetchOk(msg));\n case 'TRACK_STATUS':\n return this.frame(ControlMessageType18.TRACK_STATUS, this.encodeTrackStatus(msg));\n case 'PUBLISH_NAMESPACE':\n return this.frame(ControlMessageType18.PUBLISH_NAMESPACE, this.encodePublishNamespace(msg));\n case 'PUBLISH':\n return this.frame(ControlMessageType18.PUBLISH, this.encodePublish(msg));\n case 'PUBLISH_DONE':\n return this.frame(ControlMessageType18.PUBLISH_DONE, this.encodePublishDone(msg));\n case 'SUBSCRIBE_NAMESPACE':\n return this.frame(ControlMessageType18.SUBSCRIBE_NAMESPACE, this.encodeSubscribeNamespace(msg));\n case 'SUBSCRIBE_TRACKS':\n return this.frame(ControlMessageType18.SUBSCRIBE_TRACKS, this.encodeNamespacePrefixRequest(msg.requestId, msg.trackNamespacePrefix, msg.parameters));\n case 'NAMESPACE':\n return this.frame(ControlMessageType18.NAMESPACE, encodeTuple(msg.trackNamespaceSuffix));\n case 'NAMESPACE_DONE':\n return this.frame(ControlMessageType18.NAMESPACE_DONE, encodeTuple(msg.trackNamespaceSuffix));\n case 'PUBLISH_BLOCKED':\n return this.frame(ControlMessageType18.PUBLISH_BLOCKED, this.encodePublishBlocked(msg));\n case 'GOAWAY':\n return this.frame(ControlMessageType18.GOAWAY, this.encodeGoaway(msg));\n case 'PUBLISH_STATE_NOTIFY':\n if (this.version !== 22) {\n throw new ProtocolViolationError('Draft18Codec: PUBLISH_STATE_NOTIFY exists only in draft 22');\n }\n return this.frame(ControlMessageType18.PUBLISH_STATE_NOTIFY, this.encodeParamsOnly(msg.parameters));\n case 'PUBLISH_NAMESPACE_DONE':\n // draft-18 §3.3.2 removed this message — withdrawal is a request-stream\n // cancellation. It must never reach the wire.\n throw new ProtocolViolationError('Draft18Codec: PUBLISH_NAMESPACE_DONE was removed in draft-18');\n default:\n return notImplemented(`encode of ${msg.type}`);\n }\n }\n\n decode(buf: Uint8Array, offset: number): { message: DecodedControlMessage; bytesRead: number } {\n const { value: type, bytesRead: typeBytes } = readVi64(buf, offset);\n const lenOff = offset + typeBytes;\n if (lenOff + 2 > buf.length) throw new ProtocolViolationError('draft-18 control frame: truncated length');\n const payloadLen = (buf[lenOff]! << 8) | buf[lenOff + 1]!;\n const payloadOff = lenOff + 2;\n if (payloadOff + payloadLen > buf.length) throw new ProtocolViolationError('draft-18 control frame: truncated payload');\n const payload = buf.subarray(payloadOff, payloadOff + payloadLen);\n const total = typeBytes + 2 + payloadLen;\n\n switch (Number(type)) {\n case ControlMessageType18.SETUP:\n return { message: { type: 'SETUP', setupOptions: decodeSetupOptionMap18(payload) }, bytesRead: total };\n case ControlMessageType18.SUBSCRIBE:\n return { message: this.decodeSubscribe(payload), bytesRead: total };\n case ControlMessageType18.REQUEST_UPDATE:\n return { message: this.decodeRequestUpdate(payload), bytesRead: total };\n case ControlMessageType18.SUBSCRIBE_OK:\n return { message: this.decodeSubscribeOk(payload), bytesRead: total };\n case ControlMessageType18.REQUEST_OK:\n return { message: this.decodeRequestOk(payload), bytesRead: total };\n case ControlMessageType18.REQUEST_ERROR:\n return { message: this.decodeRequestError(payload), bytesRead: total };\n case ControlMessageType18.FETCH:\n return { message: this.version === 22 ? this.decodeFetch22(payload) : this.decodeFetch(payload), bytesRead: total };\n case ControlMessageType18.FETCH_OK:\n return { message: this.decodeFetchOk(payload), bytesRead: total };\n case ControlMessageType18.TRACK_STATUS:\n return { message: this.decodeTrackStatus(payload), bytesRead: total };\n case ControlMessageType18.PUBLISH_NAMESPACE:\n return { message: this.decodePublishNamespace(payload), bytesRead: total };\n case ControlMessageType18.PUBLISH:\n return { message: this.decodePublish(payload), bytesRead: total };\n case ControlMessageType18.PUBLISH_DONE:\n return { message: this.decodePublishDone(payload), bytesRead: total };\n case ControlMessageType18.SUBSCRIBE_NAMESPACE:\n return { message: this.decodeSubscribeNamespace(payload), bytesRead: total };\n case ControlMessageType18.SUBSCRIBE_TRACKS:\n return { message: this.decodeSubscribeTracks(payload), bytesRead: total };\n case ControlMessageType18.NAMESPACE:\n return { message: this.decodeNamespaceSuffix('NAMESPACE', payload), bytesRead: total };\n case ControlMessageType18.NAMESPACE_DONE:\n return { message: this.decodeNamespaceSuffix('NAMESPACE_DONE', payload), bytesRead: total };\n case ControlMessageType18.PUBLISH_BLOCKED:\n return { message: this.decodePublishBlocked(payload), bytesRead: total };\n case ControlMessageType18.GOAWAY:\n return { message: this.decodeGoaway(payload), bytesRead: total };\n case ControlMessageType18.PUBLISH_STATE_NOTIFY:\n if (this.version !== 22) return notImplemented(`decode of type 0x${type.toString(16)}`);\n return {\n message: { type: 'PUBLISH_STATE_NOTIFY', parameters: this.decodeParamsOnly(payload, 'PUBLISH_STATE_NOTIFY') },\n bytesRead: total,\n };\n default:\n return notImplemented(`decode of type 0x${type.toString(16)}`);\n }\n }\n\n peekFrameSize(buf: Uint8Array): number | undefined {\n let type: { bytesRead: number };\n try {\n type = readVi64(buf, 0);\n } catch {\n return undefined; // not enough bytes for the type vi64 yet\n }\n const headerLen = type.bytesRead + 2;\n if (buf.length < headerLen) return undefined;\n const payloadLen = (buf[type.bytesRead]! << 8) | buf[type.bytesRead + 1]!;\n return headerLen + payloadLen;\n }\n\n // ── framing ──────────────────────────────────────────────────────\n\n private frame(type: number, payload: Uint8Array): Uint8Array {\n if (payload.length > 0xffff) throw new ProtocolViolationError('draft-18 control payload exceeds 2^16-1');\n const out = new Uint8Array(vi64EncodingLength(BigInt(type)) + 2 + payload.length);\n let p = writeVi64(BigInt(type), out, 0);\n out[p++] = (payload.length >> 8) & 0xff;\n out[p++] = payload.length & 0xff;\n out.set(payload, p);\n return out;\n }\n\n // ── SUBSCRIBE (request, §10.7) ──────────────────────────────────────\n\n private encodeSubscribe(msg: Subscribe): Uint8Array {\n // §2.4.1: Track Namespace (0-32 non-empty fields) + Full Track Name ≤ 4096 bytes.\n validateFullTrackName(msg.trackNamespace, msg.trackName, { allowEmptyNamespace: true });\n const typed = paramsToTyped(msg.parameters, this.paramRegistry);\n const len =\n vi64EncodingLength(msg.requestId) +\n vi64TupleLength(msg.trackNamespace) +\n vi64BytesLength(msg.trackName) +\n messageParams18EncodingLength(typed, this.paramRegistry);\n const buf = new Uint8Array(len);\n let p = writeVi64(msg.requestId, buf, 0);\n p += writeVi64Tuple(msg.trackNamespace, buf, p);\n p += writeVi64Bytes(msg.trackName, buf, p);\n buf.set(encodeMessageParams18(typed, this.paramRegistry), p);\n return buf;\n }\n\n private decodeSubscribe(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const rid = readVi64(payload, p); p += rid.bytesRead;\n const ns = readVi64Tuple(payload, p); p += ns.bytesRead;\n const name = readVi64Bytes(payload, p); p += name.bytesRead;\n const params = decodeMessageParams18(payload, p, this.paramRegistry);\n p += params.bytesRead;\n // SUBSCRIBE has no field after Parameters; any trailing bytes are malformed.\n if (p !== payload.length) {\n throw new ProtocolViolationError('SUBSCRIBE: unexpected trailing bytes after parameters');\n }\n // §2.4.1: validate Track Namespace (0-32 non-empty fields) + Full Track Name ≤ 4096.\n validateFullTrackName(ns.value, name.value, { allowEmptyNamespace: true });\n return {\n type: 'SUBSCRIBE',\n requestId: rid.value,\n trackNamespace: ns.value,\n trackName: name.value,\n parameters: typedToParams(params.params),\n };\n }\n\n // ── TRACK_STATUS (request, §10.14 — same body as SUBSCRIBE) ─────────\n // First and only message on a new request stream; carries no delivery params.\n // Response is REQUEST_OK (a.k.a. TRACK_STATUS_OK) / REQUEST_ERROR.\n\n private encodeTrackStatus(msg: TrackStatus): Uint8Array {\n // §2.4.1: Track Namespace (0-32 non-empty fields) + Full Track Name ≤ 4096 bytes.\n validateFullTrackName(msg.trackNamespace, msg.trackName, { allowEmptyNamespace: true });\n const typed = paramsToTyped(msg.parameters, this.paramRegistry);\n const len =\n vi64EncodingLength(msg.requestId) +\n vi64TupleLength(msg.trackNamespace) +\n vi64BytesLength(msg.trackName) +\n messageParams18EncodingLength(typed, this.paramRegistry);\n const buf = new Uint8Array(len);\n let p = writeVi64(msg.requestId, buf, 0);\n p += writeVi64Tuple(msg.trackNamespace, buf, p);\n p += writeVi64Bytes(msg.trackName, buf, p);\n buf.set(encodeMessageParams18(typed, this.paramRegistry), p);\n return buf;\n }\n\n private decodeTrackStatus(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const rid = readVi64(payload, p); p += rid.bytesRead;\n const ns = readVi64Tuple(payload, p); p += ns.bytesRead;\n const name = readVi64Bytes(payload, p); p += name.bytesRead;\n const params = decodeMessageParams18(payload, p, this.paramRegistry);\n p += params.bytesRead;\n if (p !== payload.length) {\n throw new ProtocolViolationError('TRACK_STATUS: unexpected trailing bytes after parameters');\n }\n // §2.4.1: validate Track Namespace (0-32 non-empty fields) + Full Track Name ≤ 4096.\n validateFullTrackName(ns.value, name.value, { allowEmptyNamespace: true });\n return {\n type: 'TRACK_STATUS',\n requestId: rid.value,\n trackNamespace: ns.value,\n trackName: name.value,\n parameters: typedToParams(params.params),\n };\n }\n\n // ── PUBLISH_NAMESPACE (request, §10.15) ────────────────────────────\n // First and only message on a new request stream: Request ID, namespace tuple\n // (NO track name), params. Response is REQUEST_OK (PUBLISH_NAMESPACE_OK) /\n // REQUEST_ERROR.\n\n private encodePublishNamespace(msg: PublishNamespace): Uint8Array {\n // §2.4.1: PUBLISH_NAMESPACE carries a full Track Namespace (no track name):\n // 0-32 non-empty fields, total ≤ 4096 bytes.\n validateTrackNamespace(msg.trackNamespace, { allowEmpty: true });\n const typed = paramsToTyped(msg.parameters, this.paramRegistry);\n const len =\n vi64EncodingLength(msg.requestId) +\n vi64TupleLength(msg.trackNamespace) +\n messageParams18EncodingLength(typed, this.paramRegistry);\n const buf = new Uint8Array(len);\n let p = writeVi64(msg.requestId, buf, 0);\n p += writeVi64Tuple(msg.trackNamespace, buf, p);\n buf.set(encodeMessageParams18(typed, this.paramRegistry), p);\n return buf;\n }\n\n private decodePublishNamespace(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const rid = readVi64(payload, p); p += rid.bytesRead;\n const ns = readVi64Tuple(payload, p); p += ns.bytesRead;\n const params = decodeMessageParams18(payload, p, this.paramRegistry);\n p += params.bytesRead;\n if (p !== payload.length) {\n throw new ProtocolViolationError('PUBLISH_NAMESPACE: unexpected trailing bytes after parameters');\n }\n // §2.4.1: PUBLISH_NAMESPACE carries a full Track Namespace (no track name).\n validateTrackNamespace(ns.value, { allowEmpty: true });\n return {\n type: 'PUBLISH_NAMESPACE',\n requestId: rid.value,\n trackNamespace: ns.value,\n parameters: typedToParams(params.params),\n };\n }\n\n // ── PUBLISH (request, §10.10) ──────────────────────────────────────\n // First message on a new INBOUND bidi stream (publisher → subscriber):\n // Request ID + namespace tuple + Track Name + Track Alias (vi64) + params +\n // Track Properties (§2.5, trailing field; empty → zero bytes).\n\n private encodePublish(msg: Publish): Uint8Array {\n // §2.4.1: Track Namespace (0-32 non-empty fields) + Full Track Name ≤ 4096 bytes.\n validateFullTrackName(msg.trackNamespace, msg.trackName, { allowEmptyNamespace: true });\n const typed = paramsToTyped(msg.parameters, this.paramRegistry);\n const len =\n vi64EncodingLength(msg.requestId) +\n vi64TupleLength(msg.trackNamespace) +\n vi64BytesLength(msg.trackName) +\n vi64EncodingLength(msg.trackAlias) +\n messageParams18EncodingLength(typed, this.paramRegistry) +\n trackProperties18EncodingLength(trackPropsOf(msg));\n const buf = new Uint8Array(len);\n let p = writeVi64(msg.requestId, buf, 0);\n p += writeVi64Tuple(msg.trackNamespace, buf, p);\n p += writeVi64Bytes(msg.trackName, buf, p);\n p += writeVi64(msg.trackAlias, buf, p);\n const params = encodeMessageParams18(typed, this.paramRegistry);\n buf.set(params, p); p += params.length;\n buf.set(encodeTrackProperties18(trackPropsOf(msg)), p);\n return buf;\n }\n\n private decodePublish(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const rid = readVi64(payload, p); p += rid.bytesRead;\n const ns = readVi64Tuple(payload, p); p += ns.bytesRead;\n const name = readVi64Bytes(payload, p); p += name.bytesRead;\n const alias = readVi64(payload, p); p += alias.bytesRead;\n const params = decodeMessageParams18(payload, p, this.paramRegistry);\n p += params.bytesRead;\n const props = decodeTrackProperties18(payload, p); // remaining bytes (empty → {})\n p += props.bytesRead;\n // §2.4.1: validate Track Namespace (0-32 non-empty fields) + Full Track Name ≤ 4096.\n validateFullTrackName(ns.value, name.value, { allowEmptyNamespace: true });\n return {\n type: 'PUBLISH',\n requestId: rid.value,\n trackNamespace: ns.value,\n trackName: name.value,\n trackAlias: alias.value,\n parameters: typedToParams(params.params),\n trackProperties: props.properties, trackExtensions: props.properties,\n };\n }\n\n // ── PUBLISH_DONE (response on PUBLISH stream, §10.11 — no Request ID) ──\n // Status Code (vi64) + Stream Count (vi64) + Error Reason. The Request ID is\n // supplied by the inbound PUBLISH stream context.\n\n private encodePublishDone(msg: PublishDone): Uint8Array {\n // statusCode/streamCount are vi64 (full uint64); writeVi64/vi64EncodingLength\n // range-check (0..2^64-1), so no QUIC-Varint cap here.\n const len =\n vi64EncodingLength(msg.statusCode) +\n vi64EncodingLength(msg.streamCount) +\n reasonPhrase18Length(msg.errorReason);\n const buf = new Uint8Array(len);\n let p = writeVi64(msg.statusCode, buf, 0);\n p += writeVi64(msg.streamCount, buf, p);\n writeReasonPhrase18(msg.errorReason, buf, p);\n return buf;\n }\n\n private decodePublishDone(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const sc = readVi64(payload, p); p += sc.bytesRead;\n const cnt = readVi64(payload, p); p += cnt.bytesRead;\n const reason = readReasonPhrase18(payload, p); p += reason.bytesRead;\n if (p !== payload.length) {\n throw new ProtocolViolationError('PUBLISH_DONE: unexpected trailing bytes after Error Reason');\n }\n // Response: Request ID omitted on the wire (stream context stamps it).\n // statusCode/streamCount are vi64 (full uint64) — return the raw bigints.\n return {\n type: 'PUBLISH_DONE',\n statusCode: sc.value,\n streamCount: cnt.value,\n errorReason: reason.value,\n };\n }\n\n // ── SUBSCRIBE_NAMESPACE (request, §10.18) ──────────────────────────\n // First message on a CONTINUING request stream: Request ID, namespace prefix\n // tuple (0–32 fields), params. After REQUEST_OK the same stream carries\n // NAMESPACE / NAMESPACE_DONE messages (decoded below, no Request ID).\n\n /** Shared body for the prefix-based requests (Request ID + prefix + params). */\n private encodeNamespacePrefixRequest(\n requestId: RequestId,\n prefix: Uint8Array[],\n parameters: Parameters,\n ): Uint8Array {\n const typed = paramsToTyped(parameters, this.paramRegistry);\n const len =\n vi64EncodingLength(requestId) +\n vi64TupleLength(prefix) +\n messageParams18EncodingLength(typed, this.paramRegistry);\n const buf = new Uint8Array(len);\n let p = writeVi64(requestId, buf, 0);\n p += writeVi64Tuple(prefix, buf, p);\n buf.set(encodeMessageParams18(typed, this.paramRegistry), p);\n return buf;\n }\n\n /** Shared decode for the prefix-based requests; returns the common fields. */\n private decodeNamespacePrefixRequest(\n name: 'SUBSCRIBE_NAMESPACE' | 'SUBSCRIBE_TRACKS',\n payload: Uint8Array,\n ): { requestId: RequestId; prefix: Uint8Array[]; parameters: Parameters } {\n let p = 0;\n const rid = readVi64(payload, p); p += rid.bytesRead;\n const prefix = readVi64Tuple(payload, p); p += prefix.bytesRead; // 0 fields allowed\n const params = decodeMessageParams18(payload, p, this.paramRegistry);\n p += params.bytesRead;\n if (p !== payload.length) {\n throw new ProtocolViolationError(`${name}: unexpected trailing bytes after parameters`);\n }\n return { requestId: rid.value, prefix: prefix.value, parameters: typedToParams(params.params) };\n }\n\n private encodeSubscribeNamespace(msg: SubscribeNamespace): Uint8Array {\n return this.encodeNamespacePrefixRequest(msg.requestId, msg.trackNamespacePrefix, msg.parameters);\n }\n\n private decodeSubscribeNamespace(payload: Uint8Array): DecodedControlMessage {\n const { requestId, prefix, parameters } = this.decodeNamespacePrefixRequest('SUBSCRIBE_NAMESPACE', payload);\n return { type: 'SUBSCRIBE_NAMESPACE', requestId, trackNamespacePrefix: prefix, parameters };\n }\n\n // ── SUBSCRIBE_TRACKS (request, §10.19) / PUBLISH_BLOCKED (§10.20) ────\n // SUBSCRIBE_TRACKS shares the prefix-request body. PUBLISH_BLOCKED rides the\n // SUBSCRIBE_TRACKS response stream (no Request ID): suffix tuple + Track Name.\n\n private decodeSubscribeTracks(payload: Uint8Array): DecodedControlMessage {\n const { requestId, prefix, parameters } = this.decodeNamespacePrefixRequest('SUBSCRIBE_TRACKS', payload);\n return { type: 'SUBSCRIBE_TRACKS', requestId, trackNamespacePrefix: prefix, parameters };\n }\n\n private encodePublishBlocked(msg: PublishBlocked): Uint8Array {\n const len = vi64TupleLength(msg.trackNamespaceSuffix) + vi64BytesLength(msg.trackName);\n const buf = new Uint8Array(len);\n let p = writeVi64Tuple(msg.trackNamespaceSuffix, buf, 0);\n p += writeVi64Bytes(msg.trackName, buf, p);\n return buf;\n }\n\n private decodePublishBlocked(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const suffix = readVi64Tuple(payload, p); p += suffix.bytesRead;\n const name = readVi64Bytes(payload, p); p += name.bytesRead;\n if (p !== payload.length) {\n throw new ProtocolViolationError('PUBLISH_BLOCKED: unexpected trailing bytes after Track Name');\n }\n return { type: 'PUBLISH_BLOCKED', trackNamespaceSuffix: suffix.value, trackName: name.value };\n }\n\n // ── GOAWAY (0x10) — §10.4: URI (vi64-len), Timeout (vi64), [Request ID (vi64)] ──\n\n private encodeGoaway(msg: Goaway): Uint8Array {\n const uri = TEXT_ENCODER.encode(msg.newSessionUri);\n if (uri.length > GOAWAY_MAX_URI_LENGTH) {\n throw new ProtocolViolationError(`GOAWAY New Session URI length ${uri.length} exceeds maximum ${GOAWAY_MAX_URI_LENGTH} bytes`);\n }\n const timeout = msg.timeout ?? 0n;\n // draft-22 §9.2 dropped the Request ID.\n const requestId = this.version === 22 ? undefined : msg.requestId;\n let len = vi64BytesLength(uri) + vi64EncodingLength(timeout);\n if (requestId !== undefined) len += vi64EncodingLength(requestId);\n const buf = new Uint8Array(len);\n let p = writeVi64Bytes(uri, buf, 0);\n p += writeVi64(timeout, buf, p);\n if (requestId !== undefined) writeVi64(requestId, buf, p);\n return buf;\n }\n\n private decodeGoaway(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const uri = readVi64Bytes(payload, p); p += uri.bytesRead;\n if (uri.value.length > GOAWAY_MAX_URI_LENGTH) {\n throw new ProtocolViolationError(`GOAWAY New Session URI length ${uri.value.length} exceeds maximum ${GOAWAY_MAX_URI_LENGTH} bytes`);\n }\n const timeout = readVi64(payload, p); p += timeout.bytesRead;\n // Request ID is present only on the control stream (§10.4). The codec is\n // context-free: read it iff bytes remain, and expose it optionally.\n let requestId: bigint | undefined;\n if (p < payload.length && this.version !== 22) {\n const rid = readVi64(payload, p); p += rid.bytesRead;\n requestId = rid.value;\n }\n if (p !== payload.length) {\n throw new ProtocolViolationError(this.version === 22\n ? `GOAWAY: ${payload.length - p} trailing bytes after Timeout`\n : `GOAWAY: ${payload.length - p} trailing bytes after Request ID`);\n }\n const msg: Goaway = {\n type: 'GOAWAY',\n newSessionUri: TEXT_DECODER.decode(uri.value),\n timeout: timeout.value,\n ...(requestId !== undefined ? { requestId } : {}),\n };\n return msg;\n }\n\n // ── NAMESPACE (0x08) / NAMESPACE_DONE (0x0E) — suffix tuple only, no ID ──\n\n private decodeNamespaceSuffix(\n type: 'NAMESPACE' | 'NAMESPACE_DONE',\n payload: Uint8Array,\n ): DecodedControlMessage {\n const suffix = readVi64Tuple(payload, 0);\n if (suffix.bytesRead !== payload.length) {\n throw new ProtocolViolationError(`${type}: unexpected trailing bytes after namespace suffix`);\n }\n return { type, trackNamespaceSuffix: suffix.value } as Namespace | NamespaceDone;\n }\n\n // ── REQUEST_UPDATE (request, §10.9) ─────────────────────────────────\n // Keeps its OWN Request ID; the \"Existing Request ID\" (target) is omitted —\n // the bidirectional request stream it is sent on identifies the target.\n\n private encodeRequestUpdate(msg: RequestUpdate): Uint8Array {\n const typed = paramsToTyped(msg.parameters, this.paramRegistry);\n const len = vi64EncodingLength(msg.requestId) + messageParams18EncodingLength(typed, this.paramRegistry);\n const buf = new Uint8Array(len);\n let p = writeVi64(msg.requestId, buf, 0);\n buf.set(encodeMessageParams18(typed, this.paramRegistry), p);\n return buf;\n }\n\n private decodeRequestUpdate(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const rid = readVi64(payload, p); p += rid.bytesRead;\n const params = decodeMessageParams18(payload, p, this.paramRegistry);\n p += params.bytesRead;\n if (p !== payload.length) {\n throw new ProtocolViolationError('REQUEST_UPDATE: unexpected trailing bytes after parameters');\n }\n // existingRequestId is supplied later from the request stream's context.\n return { type: 'REQUEST_UPDATE', requestId: rid.value, parameters: typedToParams(params.params) };\n }\n\n // ── SUBSCRIBE_OK (response, §10.8 — no Request ID) ─────────────────\n\n private encodeSubscribeOk(msg: SubscribeOk): Uint8Array {\n const typed = paramsToTyped(msg.parameters, this.paramRegistry);\n const len =\n vi64EncodingLength(msg.trackAlias) +\n messageParams18EncodingLength(typed, this.paramRegistry) +\n trackProperties18EncodingLength(trackPropsOf(msg));\n const buf = new Uint8Array(len);\n let p = writeVi64(msg.trackAlias, buf, 0);\n const params = encodeMessageParams18(typed, this.paramRegistry);\n buf.set(params, p); p += params.length;\n buf.set(encodeTrackProperties18(trackPropsOf(msg)), p);\n return buf;\n }\n\n private decodeSubscribeOk(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const alias = readVi64(payload, p); p += alias.bytesRead;\n const params = decodeMessageParams18(payload, p, this.paramRegistry);\n p += params.bytesRead;\n const props = decodeTrackProperties18(payload, p); // remaining bytes (empty → {})\n p += props.bytesRead;\n // Response: Request ID is omitted on the wire (topology stamps it).\n return {\n type: 'SUBSCRIBE_OK',\n trackAlias: alias.value,\n parameters: typedToParams(params.params),\n trackProperties: props.properties, trackExtensions: props.properties,\n };\n }\n\n // ── REQUEST_OK (response, §10.5 — no Request ID) ───────────────────\n\n private encodeRequestOk(msg: RequestOk): Uint8Array {\n const typed = paramsToTyped(msg.parameters, this.paramRegistry);\n const props = trackPropsOf(msg);\n const params = encodeMessageParams18(typed, this.paramRegistry);\n const propBytes = encodeTrackProperties18(props);\n const buf = new Uint8Array(params.length + propBytes.length);\n buf.set(params, 0);\n buf.set(propBytes, params.length);\n return buf;\n }\n\n private decodeRequestOk(payload: Uint8Array): DecodedControlMessage {\n const params = decodeMessageParams18(payload, 0, this.paramRegistry);\n // Track Properties are CONTEXT-dependent on a REQUEST_OK: valid for a\n // TRACK_STATUS_OK, but not for PUBLISH_NAMESPACE / SUBSCRIBE_NAMESPACE /\n // SUBSCRIBE_TRACKS responses. The codec is context-free, so it decodes them\n // unconditionally; the session validates them against the request-stream\n // context (which request this REQUEST_OK answers).\n const props = decodeTrackProperties18(payload, params.bytesRead);\n // Response: Request ID omitted on the wire (topology stamps it).\n return { type: 'REQUEST_OK', parameters: typedToParams(params.params), trackProperties: props.properties, trackExtensions: props.properties };\n }\n\n // ── REQUEST_ERROR (response, §10.6 — no Request ID) ────────────────\n\n private encodeRequestError(msg: RequestErrorMsg): Uint8Array {\n // §10.6.2: Error Code and Retry Interval are vi64 (full uint64, NOT QUIC-capped).\n // §10.6.2: a Redirect is present iff the Error Code is REDIRECT (0x34).\n const isRedirect = msg.errorCode === REDIRECT_ERROR_CODE;\n if (msg.redirect && !isRedirect) {\n throw new ProtocolViolationError('REQUEST_ERROR carries a Redirect but Error Code is not REDIRECT (0x34)');\n }\n if (isRedirect && !msg.redirect) {\n throw new ProtocolViolationError('REQUEST_ERROR with Error Code REDIRECT (0x34) requires a Redirect structure');\n }\n let redirectLen = 0;\n if (msg.redirect) {\n validateTrackNamespacePrefix(msg.redirect.trackNamespace); // size limits (0-32 fields, ≤4096 bytes)\n redirectLen =\n vi64BytesLength(msg.redirect.connectUri) +\n vi64TupleLength(msg.redirect.trackNamespace) +\n vi64BytesLength(msg.redirect.trackName);\n }\n\n const len =\n vi64EncodingLength(msg.errorCode) +\n vi64EncodingLength(msg.retryInterval) +\n reasonPhrase18Length(msg.errorReason) +\n redirectLen;\n const buf = new Uint8Array(len);\n let p = writeVi64(msg.errorCode, buf, 0);\n p += writeVi64(msg.retryInterval, buf, p);\n p += writeReasonPhrase18(msg.errorReason, buf, p);\n if (msg.redirect) {\n p += writeVi64Bytes(msg.redirect.connectUri, buf, p);\n p += writeVi64Tuple(msg.redirect.trackNamespace, buf, p);\n writeVi64Bytes(msg.redirect.trackName, buf, p);\n }\n return buf;\n }\n\n private decodeRequestError(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const ec = readVi64(payload, p); p += ec.bytesRead;\n const ri = readVi64(payload, p); p += ri.bytesRead;\n const reason = readReasonPhrase18(payload, p); p += reason.bytesRead;\n\n // §10.6.2: the optional Redirect is present iff Error Code is REDIRECT (0x34).\n // A truncated Redirect throws via the vi64 helpers; other contexts are decided\n // by the session (the response stream knows which request this answers).\n let redirect: { connectUri: Uint8Array; trackNamespace: Uint8Array[]; trackName: Uint8Array } | undefined;\n if (ec.value === REDIRECT_ERROR_CODE) {\n const uri = readVi64Bytes(payload, p); p += uri.bytesRead;\n const ns = readVi64Tuple(payload, p); p += ns.bytesRead;\n const name = readVi64Bytes(payload, p); p += name.bytesRead;\n if (p !== payload.length) {\n throw new ProtocolViolationError('REQUEST_ERROR Redirect: unexpected trailing bytes after Track Name');\n }\n validateTrackNamespacePrefix(ns.value); // size limits (0-32 fields, ≤4096 bytes)\n redirect = { connectUri: uri.value, trackNamespace: ns.value, trackName: name.value };\n } else if (p < payload.length) {\n // Trailing bytes on a non-REDIRECT error are a Redirect that does not belong.\n throw new ProtocolViolationError('REQUEST_ERROR: trailing bytes (a Redirect is only valid with Error Code REDIRECT)');\n }\n\n return {\n type: 'REQUEST_ERROR',\n // §10.6.2: Error Code / Retry Interval are vi64 (full uint64) — accepted raw,\n // not folded through the QUIC-Varint range.\n errorCode: ec.value,\n retryInterval: ri.value,\n errorReason: reason.value,\n ...(redirect ? { redirect } : {}),\n };\n }\n\n // ── FETCH (request, §10.12) ─────────────────────────────────────────\n // First message on a new bidi request stream. Keeps its Request ID. The\n // Standalone body uses vi64 Location fields (full uint64, no QUIC cap).\n\n private encodeFetch(msg: Fetch): Uint8Array {\n const typed = paramsToTyped(msg.parameters, this.paramRegistry);\n const f = msg.fetch;\n let bodyLen: number;\n if (f.fetchType === 0x1) {\n // §2.4.1: a standalone FETCH carries a Full Track Name (≤ 4096 bytes, 0-32\n // non-empty namespace fields). Joining fetches carry no name.\n validateFullTrackName(f.trackNamespace, f.trackName, { allowEmptyNamespace: true });\n bodyLen =\n vi64TupleLength(f.trackNamespace) +\n vi64BytesLength(f.trackName) +\n vi64LocationLength(f.startLocation) +\n vi64LocationLength(f.endLocation);\n } else {\n // Joining Start is vi64 (full uint64) in draft-18 — no QUIC-range cap.\n bodyLen = vi64EncodingLength(f.joiningRequestId) + vi64EncodingLength(f.joiningStart);\n }\n const len =\n vi64EncodingLength(msg.requestId) +\n vi64EncodingLength(BigInt(f.fetchType)) +\n bodyLen +\n messageParams18EncodingLength(typed, this.paramRegistry);\n const buf = new Uint8Array(len);\n let p = writeVi64(msg.requestId, buf, 0);\n p += writeVi64(BigInt(f.fetchType), buf, p);\n if (f.fetchType === 0x1) {\n p += writeVi64Tuple(f.trackNamespace, buf, p);\n p += writeVi64Bytes(f.trackName, buf, p);\n p += writeVi64Location(f.startLocation, buf, p);\n p += writeVi64Location(f.endLocation, buf, p);\n } else {\n p += writeVi64(f.joiningRequestId, buf, p);\n p += writeVi64(f.joiningStart, buf, p);\n }\n buf.set(encodeMessageParams18(typed, this.paramRegistry), p);\n return buf;\n }\n\n private decodeFetch(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const rid = readVi64(payload, p); p += rid.bytesRead;\n const ft = readVi64(payload, p); p += ft.bytesRead;\n const fetchType = Number(ft.value);\n\n let fetch: StandaloneFetch | JoiningFetch;\n if (fetchType === 0x1) {\n const ns = readVi64Tuple(payload, p); p += ns.bytesRead;\n const name = readVi64Bytes(payload, p); p += name.bytesRead;\n const start = readVi64Location(payload, p); p += start.bytesRead;\n const end = readVi64Location(payload, p); p += end.bytesRead;\n // §2.4.1: a standalone FETCH carries a Full Track Name (≤ 4096 bytes, 0-32\n // non-empty namespace fields).\n validateFullTrackName(ns.value, name.value, { allowEmptyNamespace: true });\n fetch = {\n fetchType: 0x1,\n trackNamespace: ns.value,\n trackName: name.value,\n startLocation: start.value,\n endLocation: end.value,\n };\n } else if (fetchType === 0x2 || fetchType === 0x3) {\n const jrid = readVi64(payload, p); p += jrid.bytesRead;\n const jstart = readVi64(payload, p); p += jstart.bytesRead;\n // Joining Start is full-uint64 vi64 in draft-18 — kept as a raw bigint.\n fetch = { fetchType, joiningRequestId: jrid.value, joiningStart: jstart.value };\n } else {\n throw new ProtocolViolationError(`FETCH: invalid Fetch Type 0x${ft.value.toString(16)}`);\n }\n\n const params = decodeMessageParams18(payload, p, this.paramRegistry);\n p += params.bytesRead;\n if (p !== payload.length) {\n throw new ProtocolViolationError('FETCH: unexpected trailing bytes after parameters');\n }\n return { type: 'FETCH', requestId: rid.value, fetch, parameters: typedToParams(params.params) };\n }\n\n // ── draft-22 FETCH (§9.11): Request ID, Full Track Name, Parameters ──\n // The range is a LOCATION_FILTER parameter. A StandaloneFetch's draft-18\n // range maps onto it: the draft-18 End Location's Object is exclusive, with 0\n // meaning the whole End Group, while the LOCATION_FILTER's EndObject is\n // inclusive and omitted for a whole group. There is no Joining FETCH: a\n // subscriber asks for the prefix with FILL_PARAMETERS on its SUBSCRIBE.\n\n private encodeFetch22(msg: Fetch): Uint8Array {\n const f = msg.fetch;\n if (f.fetchType !== 0x1) {\n throw new ProtocolViolationError('draft-22 has no Joining FETCH; request the prefix with FILL_PARAMETERS');\n }\n validateFullTrackName(f.trackNamespace, f.trackName, { allowEmptyNamespace: true });\n const parameters = new Map(msg.parameters);\n if (!parameters.has(LOCATION_FILTER_TYPE)) {\n const { startLocation: start, endLocation: end } = f;\n if (end.group < start.group) {\n throw new ProtocolViolationError(`FETCH End Group ${end.group} < Start Group ${start.group}`);\n }\n const fields = [start.group, start.object, end.group - start.group];\n if (end.object > 0n) fields.push(end.object - 1n);\n parameters.set(LOCATION_FILTER_TYPE, [encodeFetchLocationFilter(fields)]);\n }\n const typed = paramsToTyped(parameters, this.paramRegistry);\n const len =\n vi64EncodingLength(msg.requestId) +\n vi64TupleLength(f.trackNamespace) +\n vi64BytesLength(f.trackName) +\n messageParams18EncodingLength(typed, this.paramRegistry);\n const buf = new Uint8Array(len);\n let p = writeVi64(msg.requestId, buf, 0);\n p += writeVi64Tuple(f.trackNamespace, buf, p);\n p += writeVi64Bytes(f.trackName, buf, p);\n buf.set(encodeMessageParams18(typed, this.paramRegistry), p);\n return buf;\n }\n\n /**\n * Decodes a draft-22 FETCH into a StandaloneFetch. An absolute LOCATION_FILTER\n * with an end maps back to the draft-18 range; a filter that depends on the\n * Largest Object (one field, or two fields with no end) or no filter at all\n * gets a start of {0, 0} or its StartGroup/StartObject and an open end\n * ({2^64-1, 0}), and the LOCATION_FILTER stays in `parameters` for the\n * receiver to resolve.\n */\n private decodeFetch22(payload: Uint8Array): DecodedControlMessage {\n let p = 0;\n const rid = readVi64(payload, p); p += rid.bytesRead;\n const ns = readVi64Tuple(payload, p); p += ns.bytesRead;\n const name = readVi64Bytes(payload, p); p += name.bytesRead;\n validateFullTrackName(ns.value, name.value, { allowEmptyNamespace: true });\n const params = decodeMessageParams18(payload, p, this.paramRegistry);\n p += params.bytesRead;\n if (p !== payload.length) {\n throw new ProtocolViolationError('FETCH: unexpected trailing bytes after parameters');\n }\n const parameters = typedToParams(params.params);\n const filter = parameters.get(LOCATION_FILTER_TYPE)?.[0];\n let fields: bigint[] = [];\n if (filter !== undefined) {\n if (!(filter instanceof Uint8Array)) throw new ProtocolViolationError('FETCH: LOCATION_FILTER is not bytes');\n try {\n fields = decodeFetchLocationFilter(filter);\n } catch (e) {\n throw new ProtocolViolationError(`FETCH: ${(e as Error).message}`);\n }\n }\n const openEnd = { group: MAX_VI64, object: 0n };\n let startLocation = { group: 0n, object: 0n };\n let endLocation = openEnd;\n if (fields.length === 2) {\n startLocation = { group: fields[0]!, object: fields[1]! };\n } else if (fields.length >= 3) {\n startLocation = { group: fields[0]!, object: fields[1]! };\n const endGroup = fields[0]! + fields[2]!;\n endLocation = { group: endGroup, object: fields.length === 4 ? fields[3]! + 1n : 0n };\n }\n return {\n type: 'FETCH',\n requestId: rid.value,\n fetch: { fetchType: 0x1, trackNamespace: ns.value, trackName: name.value, startLocation, endLocation },\n parameters,\n };\n }\n\n // ── draft-22 PUBLISH_STATE_NOTIFY (§9.10): Parameters only ──────────\n\n private encodeParamsOnly(parameters: Parameters): Uint8Array {\n const typed = paramsToTyped(parameters, this.paramRegistry);\n return encodeMessageParams18(typed, this.paramRegistry);\n }\n\n private decodeParamsOnly(payload: Uint8Array, what: string): Parameters {\n const params = decodeMessageParams18(payload, 0, this.paramRegistry);\n if (params.bytesRead !== payload.length) {\n throw new ProtocolViolationError(`${what}: unexpected trailing bytes after parameters`);\n }\n return typedToParams(params.params);\n }\n\n // ── FETCH_OK (response, §10.13 — no Request ID) ────────────────────\n // Sent as the first message on the same bidi stream; the topology stamps the\n // Request ID from the stream context. Track Properties (§2.5) trail the params.\n\n private encodeFetchOk(msg: FetchOk): Uint8Array {\n if (msg.endOfTrack !== 0 && msg.endOfTrack !== 1) {\n throw new ProtocolViolationError(`FETCH_OK End Of Track must be 0 or 1, got ${msg.endOfTrack}`);\n }\n const typed = paramsToTyped(msg.parameters, this.paramRegistry);\n const len =\n 1 + vi64LocationLength(msg.endLocation) +\n messageParams18EncodingLength(typed, this.paramRegistry) +\n trackProperties18EncodingLength(trackPropsOf(msg));\n const buf = new Uint8Array(len);\n let p = 0;\n buf[p++] = msg.endOfTrack;\n p += writeVi64Location(msg.endLocation, buf, p);\n const params = encodeMessageParams18(typed, this.paramRegistry);\n buf.set(params, p); p += params.length;\n buf.set(encodeTrackProperties18(trackPropsOf(msg)), p);\n return buf;\n }\n\n private decodeFetchOk(payload: Uint8Array): DecodedControlMessage {\n if (payload.length < 1) throw new ProtocolViolationError('FETCH_OK: truncated');\n let p = 0;\n const endOfTrack = payload[p++]!;\n if (endOfTrack !== 0 && endOfTrack !== 1) {\n throw new ProtocolViolationError(`FETCH_OK End Of Track must be 0 or 1, got ${endOfTrack}`);\n }\n const end = readVi64Location(payload, p); p += end.bytesRead;\n const params = decodeMessageParams18(payload, p, this.paramRegistry);\n p += params.bytesRead;\n const props = decodeTrackProperties18(payload, p); // remaining bytes (empty → {})\n p += props.bytesRead;\n // Response: Request ID omitted on the wire (topology stamps it).\n return {\n type: 'FETCH_OK',\n endOfTrack,\n endLocation: end.value,\n parameters: typedToParams(params.params),\n trackProperties: props.properties, trackExtensions: props.properties,\n };\n }\n}\n","/**\n * Data plane type codes and constants.\n * @see draft-ietf-moq-transport-16 §10\n * @module\n */\n\nimport { varint } from '../primitives/varint.js';\n\n/**\n * Object Status codes.\n * @see draft-ietf-moq-transport-16 §10.2.1.1\n */\nexport const ObjectStatus = {\n /** Normal object (implicit for non-zero length payloads). */\n NORMAL: varint(0x0),\n /**\n * Object does not exist at any publisher. Valid in draft-14.\n * @see draft-ietf-moq-transport-14 §10.2.1.1\n */\n OBJECT_DOES_NOT_EXIST: varint(0x1),\n /** No objects with same Group ID and Object ID >= this exist. */\n END_OF_GROUP: varint(0x3),\n /** No objects at location >= this exist. */\n END_OF_TRACK: varint(0x4),\n} as const;\n\n/**\n * Data stream header types.\n * @see draft-ietf-moq-transport-16 §10.4\n */\nexport const DataStreamType = {\n /** FETCH_HEADER - fixed type byte. */\n FETCH_HEADER: 0x05,\n} as const;\n\n/**\n * SUBGROUP_HEADER type byte bit flags.\n * Type byte format: 0b00X1XXXX (bit 4 always set).\n * Valid ranges: 0x10-0x1F, 0x30-0x3F.\n * @see draft-ietf-moq-transport-16 §10.4.2\n */\nexport const SubgroupFlags = {\n /** Extensions present in all objects on this stream. */\n EXTENSIONS: 0x01,\n /** Subgroup ID mode mask (bits 1-2). */\n SUBGROUP_ID_MODE_MASK: 0x06,\n /** Subgroup contains largest object in group. */\n END_OF_GROUP: 0x08,\n /** Bit 4 - always set for subgroup headers. */\n SUBGROUP_MARKER: 0x10,\n /** Priority field omitted (inherit from subscription). */\n DEFAULT_PRIORITY: 0x20,\n} as const;\n\n/**\n * Subgroup ID modes (extracted from type byte bits 1-2).\n * @see draft-ietf-moq-transport-16 §10.4.2\n */\nexport const SubgroupIdMode = {\n /** Subgroup ID = 0 (field absent). */\n ZERO: 0b00,\n /** Subgroup ID = first Object ID in subgroup (field absent). */\n FIRST_OBJECT: 0b01,\n /** Subgroup ID field present in header. */\n EXPLICIT: 0b10,\n /** Reserved - PROTOCOL_VIOLATION if received. */\n RESERVED: 0b11,\n} as const;\n\n/**\n * OBJECT_DATAGRAM type byte bit flags.\n * Type byte format: 0b00X0XXXX (bit 4 always clear).\n * Valid ranges: 0x00-0x0F, 0x20-0x2F.\n * @see draft-ietf-moq-transport-16 §10.3.1\n */\nexport const DatagramFlags = {\n /** Extensions field present. */\n EXTENSIONS: 0x01,\n /** No object with same Group ID and Object ID > current exists. */\n END_OF_GROUP: 0x02,\n /** Object ID field omitted (Object ID = 1). */\n ZERO_OBJECT_ID: 0x04,\n /** Priority field omitted (inherit from subscription). */\n DEFAULT_PRIORITY: 0x08,\n /** Object Status present instead of payload. */\n STATUS: 0x20,\n} as const;\n\n/**\n * FETCH object serialization flags.\n * @see draft-ietf-moq-transport-16 §10.4.4\n */\nexport const FetchFlags = {\n /** Subgroup encoding mode mask (bits 0-1). */\n SUBGROUP_MODE_MASK: 0x03,\n /** Object ID field present. */\n OBJECT_ID: 0x04,\n /** Group ID field present. */\n GROUP_ID: 0x08,\n /** Priority field present. */\n PRIORITY: 0x10,\n /** Extensions field present. */\n EXTENSIONS: 0x20,\n /** Datagram object (ignore subgroup bits). */\n DATAGRAM: 0x40,\n} as const;\n\n/**\n * Fetch subgroup encoding modes (bits 0-1 of serialization flags).\n * @see draft-ietf-moq-transport-16 §10.4.4.1\n */\nexport const FetchSubgroupMode = {\n /** Subgroup ID = 0. */\n ZERO: 0x00,\n /** Subgroup ID = prior object's Subgroup ID. */\n PRIOR: 0x01,\n /** Subgroup ID = prior object's Subgroup ID + 1. */\n PRIOR_PLUS_ONE: 0x02,\n /** Subgroup ID field present. */\n EXPLICIT: 0x03,\n} as const;\n\n/**\n * Special serialization flag values for FETCH streams.\n * @see draft-ietf-moq-transport-16 §10.4.4\n */\nexport const FetchSpecialFlags = {\n /** End of Non-Existent Range - objects in range don't exist. */\n END_NON_EXISTENT: 0x8c,\n /** End of Unknown Range - object status unknown. */\n END_UNKNOWN: 0x10c,\n} as const;\n\n/**\n * Check if a type byte is a valid SUBGROUP_HEADER type.\n *\n * Draft-16: 0b00X1XXXX (0x10-0x1F, 0x30-0x3F).\n * Draft-14: 0x10..0x1D only (12 types, no DEFAULT_PRIORITY range).\n *\n * @see draft-ietf-moq-transport-16 §10.4.2\n * @see draft-ietf-moq-transport-14 §10.4.2\n */\nexport function isSubgroupHeaderType(type: number, version: 14 | 16 = 16): boolean {\n if (version === 14) {\n // Draft-14: 0x10..0x1D (12 defined types, mode 0b11 reserved = 0x16/0x17 excluded)\n return type >= 0x10 && type <= 0x1d && (type & 0x06) !== 0x06;\n }\n // Draft-16: bit 4 set, bits 6-7 clear, max 0x3F\n return type <= 0x3f && (type & 0x10) !== 0 && (type & 0xc0) === 0;\n}\n\n/**\n * Check if a type byte is a valid OBJECT_DATAGRAM type.\n *\n * Draft-16: 0b00X0XXXX (0x00-0x0F, 0x20-0x2F).\n * Draft-14: 0x0-0x7, 0x20-0x21 (10 defined types, no DEFAULT_PRIORITY bit).\n *\n * @see draft-ietf-moq-transport-16 §10.3.1\n * @see draft-ietf-moq-transport-14 §10.3.1\n */\nexport function isDatagramType(type: number, version: 14 | 16 = 16): boolean {\n if (version === 14) {\n // Draft-14: 0x0-0x7 (8 types) and 0x20-0x21 (2 types)\n return (type >= 0x00 && type <= 0x07) || (type >= 0x20 && type <= 0x21);\n }\n // Draft-16: bit 4 clear, bits 6-7 clear, max 0x2F\n return type <= 0x2f && (type & 0x10) === 0 && (type & 0xc0) === 0;\n}\n\n/**\n * Check if a SUBGROUP_HEADER type byte has a valid subgroup ID mode.\n * Mode 0b11 is reserved and causes PROTOCOL_VIOLATION.\n */\nexport function isValidSubgroupIdMode(type: number): boolean {\n const mode = (type & SubgroupFlags.SUBGROUP_ID_MODE_MASK) >> 1;\n return mode !== SubgroupIdMode.RESERVED;\n}\n\n/**\n * Check if a DATAGRAM type byte has invalid flag combinations.\n * STATUS + END_OF_GROUP together is invalid.\n */\nexport function isValidDatagramFlags(type: number): boolean {\n const hasStatus = (type & DatagramFlags.STATUS) !== 0;\n const hasEndOfGroup = (type & DatagramFlags.END_OF_GROUP) !== 0;\n // STATUS and END_OF_GROUP cannot both be set\n return !(hasStatus && hasEndOfGroup);\n}\n\n/**\n * Extract subgroup ID mode from SUBGROUP_HEADER type byte.\n */\nexport function getSubgroupIdMode(type: number): number {\n return (type & SubgroupFlags.SUBGROUP_ID_MODE_MASK) >> 1;\n}\n","/**\n * Data plane decoders.\n * @see draft-ietf-moq-transport-16 §10\n * @module\n */\n\nimport { readVarint, varint, type Varint } from '../primitives/varint.js';\nimport { readUint8 } from '../primitives/bytes.js';\nimport { ProtocolViolationError } from '../errors.js';\nimport {\n DataStreamType,\n SubgroupFlags,\n DatagramFlags,\n ObjectStatus,\n FetchFlags,\n FetchSubgroupMode,\n FetchSpecialFlags,\n isSubgroupHeaderType,\n isDatagramType,\n isValidSubgroupIdMode,\n isValidDatagramFlags,\n getSubgroupIdMode,\n SubgroupIdMode,\n} from './codes.js';\nimport type {\n SubgroupHeader,\n SubgroupObject,\n FetchHeader,\n FetchObject,\n FetchEndOfRange,\n ObjectDatagram,\n} from './types.js';\n\n/**\n * Valid Object Status codes per §10.2.1.1.\n *\n * Draft-16: 0x0 (Normal), 0x3 (END_OF_GROUP), 0x4 (END_OF_TRACK).\n * Draft-14: also includes 0x1 (OBJECT_DOES_NOT_EXIST).\n *\n * @see draft-ietf-moq-transport-16 §10.2.1.1\n * @see draft-ietf-moq-transport-14 §10.2.1.1\n */\nconst VALID_STATUS_CODES_V16 = new Set([\n ObjectStatus.NORMAL,\n ObjectStatus.END_OF_GROUP,\n ObjectStatus.END_OF_TRACK,\n]);\n\nconst VALID_STATUS_CODES_V14 = new Set([\n ObjectStatus.NORMAL,\n ObjectStatus.OBJECT_DOES_NOT_EXIST,\n ObjectStatus.END_OF_GROUP,\n ObjectStatus.END_OF_TRACK,\n]);\n\n/**\n * Validate that a status code is one of the allowed values for the given draft version.\n * @throws {ProtocolViolationError} If status is not valid for the version\n */\nfunction validateObjectStatus(status: Varint, version: 14 | 16 = 16): void {\n const validCodes = version === 14 ? VALID_STATUS_CODES_V14 : VALID_STATUS_CODES_V16;\n if (!validCodes.has(status)) {\n const allowed = version === 14 ? '0x0, 0x1, 0x3, 0x4' : '0x0, 0x3, 0x4';\n throw new ProtocolViolationError(\n `Invalid Object Status code: 0x${status.toString(16)} (allowed: ${allowed})`,\n );\n }\n}\n\n/**\n * Decode a SUBGROUP_HEADER from a data stream.\n * @param buf Buffer containing the header\n * @param offset Starting position\n * @returns Decoded header and bytes consumed\n * @throws {Error} If type byte is invalid or uses reserved mode\n * @see draft-ietf-moq-transport-16 §10.4.2\n */\nexport function decodeSubgroupHeader(\n buf: Uint8Array,\n offset: number,\n version: 14 | 16 = 16,\n): { header: SubgroupHeader; bytesRead: number } {\n let pos = offset;\n\n // Read type byte (varint, but typically 1 byte)\n const { value: typeVarint, bytesRead: typeBytes } = readVarint(buf, pos);\n pos += typeBytes;\n const typeByte = Number(typeVarint);\n\n // Validate type byte\n if (!isSubgroupHeaderType(typeByte, version)) {\n throw new ProtocolViolationError(\n `Invalid SUBGROUP_HEADER type byte: 0x${typeByte.toString(16)}`,\n );\n }\n\n // Check for reserved subgroup ID mode\n if (!isValidSubgroupIdMode(typeByte)) {\n throw new ProtocolViolationError(\n `Reserved subgroup ID mode (0b11) in type byte: 0x${typeByte.toString(16)}`,\n );\n }\n\n // Read track alias\n const { value: trackAlias, bytesRead: taBytes } = readVarint(buf, pos);\n pos += taBytes;\n\n // Read group ID\n const { value: groupId, bytesRead: gidBytes } = readVarint(buf, pos);\n pos += gidBytes;\n\n // Read subgroup ID based on mode\n const mode = getSubgroupIdMode(typeByte);\n let subgroupId: Varint;\n\n if (mode === SubgroupIdMode.EXPLICIT) {\n // Mode 2: explicit subgroup ID field\n const { value: sgid, bytesRead: sgidBytes } = readVarint(buf, pos);\n pos += sgidBytes;\n subgroupId = sgid;\n } else {\n // Modes 0 (ZERO) and 1 (FIRST_OBJECT) both produce subgroupId=0.\n // For ZERO, 0 is the final value. For FIRST_OBJECT, 0 is a placeholder\n // that the caller overwrites when the first object is decoded.\n // Keeping the two modes collapsed here — a separate branch on\n // `mode === ZERO` would be behaviorally indistinguishable from its\n // sibling (both set subgroupId=0), which Stryker flags as an\n // equivalent-mutant survivor. Collapsing removes the noise.\n subgroupId = varint(0);\n }\n\n // Read publisher priority.\n // Draft-14 §10.4.2: Publisher Priority is always present.\n // Draft-16: only present when DEFAULT_PRIORITY bit is NOT set.\n let publisherPriority: number | undefined;\n if (version === 14 || (typeByte & SubgroupFlags.DEFAULT_PRIORITY) === 0) {\n const { value: prio } = readUint8(buf, pos);\n pos += 1;\n publisherPriority = prio;\n }\n\n // Extract flags\n const hasExtensions = (typeByte & SubgroupFlags.EXTENSIONS) !== 0;\n const isEndOfGroup = (typeByte & SubgroupFlags.END_OF_GROUP) !== 0;\n\n const header: SubgroupHeader = {\n typeByte,\n trackAlias,\n groupId,\n subgroupId,\n publisherPriority,\n hasExtensions,\n isEndOfGroup,\n };\n\n return { header, bytesRead: pos - offset };\n}\n\n/**\n * Decode a single object from a subgroup stream.\n * @param buf Buffer containing the object\n * @param offset Starting position\n * @param hasExtensions Whether extensions are present (from header)\n * @param previousObjectId Previous object ID for delta calculation\n * @param isFirstObject True if this is the first object in the subgroup\n * @returns Decoded object and bytes consumed\n * @throws {RangeError} If extension or payload length exceeds buffer\n * @throws {Error} If extensions length is 0 when EXTENSIONS flag set, or invalid status\n * @see draft-ietf-moq-transport-16 §10.4.2\n */\nexport function decodeSubgroupObject(\n buf: Uint8Array,\n offset: number,\n hasExtensions: boolean,\n previousObjectId: bigint,\n isFirstObject: boolean = true,\n version: 14 | 16 = 16,\n): { object: SubgroupObject; bytesRead: number } {\n let pos = offset;\n\n // Read object ID delta\n const { value: delta, bytesRead: deltaBytes } = readVarint(buf, pos);\n pos += deltaBytes;\n\n // Compute actual object ID\n // First object: ID = delta\n // Subsequent: ID = previousObjectId + delta + 1\n let objectId: Varint;\n if (isFirstObject) {\n objectId = delta;\n } else {\n objectId = varint(previousObjectId + delta + 1n);\n }\n\n // Read extensions if present\n // §10.4.2: \"Objects with no extensions set Extension Headers Length to 0\"\n let extensions: Uint8Array | undefined;\n let extensionsLength = 0;\n if (hasExtensions) {\n const { value: extLen, bytesRead: extLenBytes } = readVarint(buf, pos);\n pos += extLenBytes;\n extensionsLength = Number(extLen);\n\n // Bounds check\n if (pos + extensionsLength > buf.length) {\n throw new RangeError(\n `Extension length ${extensionsLength} exceeds remaining buffer (${buf.length - pos} bytes)`,\n );\n }\n\n extensions = buf.slice(pos, pos + extensionsLength);\n pos += extensionsLength;\n }\n\n // Read payload length\n const { value: payloadLen, bytesRead: plBytes } = readVarint(buf, pos);\n pos += plBytes;\n const numPayloadLen = Number(payloadLen);\n\n // Read status if payload length is 0, otherwise read payload\n let payload: Uint8Array;\n let status: Varint | undefined;\n\n if (numPayloadLen === 0) {\n payload = new Uint8Array(0);\n const { value: st, bytesRead: stBytes } = readVarint(buf, pos);\n pos += stBytes;\n status = st;\n\n // Validate status code (version-aware: draft-14 allows 0x1)\n validateObjectStatus(status, version);\n\n // §10.2.1.2: Extensions on non-Normal status objects is PROTOCOL_VIOLATION\n if (extensionsLength > 0 && status !== ObjectStatus.NORMAL) {\n throw new ProtocolViolationError(\n `Extensions present on status object (status=0x${status.toString(16)}) per §10.2.1.2`,\n );\n }\n } else {\n // Bounds check for payload\n if (pos + numPayloadLen > buf.length) {\n throw new RangeError(\n `Payload length ${numPayloadLen} exceeds remaining buffer (${buf.length - pos} bytes)`,\n );\n }\n\n payload = buf.slice(pos, pos + numPayloadLen);\n pos += numPayloadLen;\n }\n\n const object: SubgroupObject = {\n objectId,\n extensions,\n payload,\n status,\n };\n\n return { object, bytesRead: pos - offset };\n}\n\n/**\n * Decode a FETCH_HEADER from a data stream.\n * @param buf Buffer containing the header\n * @param offset Starting position\n * @returns Decoded header and bytes consumed\n * @throws {Error} If type byte is not FETCH_HEADER (0x05)\n * @see draft-ietf-moq-transport-16 §10.4.4\n */\nexport function decodeFetchHeader(\n buf: Uint8Array,\n offset: number,\n): { header: FetchHeader; bytesRead: number } {\n let pos = offset;\n\n // Read type byte\n const { value: typeVarint, bytesRead: typeBytes } = readVarint(buf, pos);\n pos += typeBytes;\n const typeByte = Number(typeVarint);\n\n if (typeByte !== DataStreamType.FETCH_HEADER) {\n throw new ProtocolViolationError(\n `Invalid FETCH_HEADER type byte: expected 0x05, got 0x${typeByte.toString(16)}`,\n );\n }\n\n // Read request ID\n const { value: requestId, bytesRead: ridBytes } = readVarint(buf, pos);\n pos += ridBytes;\n\n const header: FetchHeader = { requestId };\n\n return { header, bytesRead: pos - offset };\n}\n\n/**\n * Context for tracking prior object state in fetch streams.\n * Used for field inheritance when flags don't include explicit fields.\n */\nexport interface FetchPriorContext {\n readonly groupId: bigint;\n readonly subgroupId: bigint;\n readonly objectId: bigint;\n readonly priority?: number;\n}\n\n/**\n * Discriminated result type for fetch object decoding.\n * Either a normal FetchObject or a FetchEndOfRange marker.\n */\nexport type DecodedFetchItem = FetchObject | FetchEndOfRange;\n\n/**\n * Check if serialization flags represent an End of Range marker.\n */\nfunction isEndOfRangeFlags(flags: Varint): boolean {\n return (\n flags === BigInt(FetchSpecialFlags.END_NON_EXISTENT) ||\n flags === BigInt(FetchSpecialFlags.END_UNKNOWN)\n );\n}\n\n/**\n * Validate that serialization flags are in allowed range.\n * Valid: 0x00-0x7F, 0x8C, 0x10C. All else is PROTOCOL_VIOLATION.\n * @throws {Error} If flags are invalid\n */\nfunction validateFetchFlags(flags: Varint): void {\n // Special flags\n if (isEndOfRangeFlags(flags)) {\n return;\n }\n\n // Normal flags: 0x00-0x7F\n if (flags >= 0n && flags <= 0x7fn) {\n return;\n }\n\n throw new ProtocolViolationError(\n `Invalid fetch serialization flags 0x${flags.toString(16)} (allowed: 0x00-0x7F, 0x8C, 0x10C)`,\n );\n}\n\n/**\n * Validate that first object doesn't try to inherit fields.\n * Per §10.4.4.1 Table 6: any flag that references prior object is PROTOCOL_VIOLATION.\n * @throws {Error} If first object has inheritance flags set\n */\nfunction validateFirstFetchObject(flags: Varint): void {\n const numFlags = Number(flags);\n const isDatagram = (numFlags & FetchFlags.DATAGRAM) !== 0;\n\n // GROUP_ID must be present (absent = \"prior Object's Group ID\")\n if ((numFlags & FetchFlags.GROUP_ID) === 0) {\n throw new ProtocolViolationError(\n `First fetch object cannot inherit Group ID (GROUP_ID flag must be set)`,\n );\n }\n\n // OBJECT_ID must be present (absent = \"prior Object's Object ID + 1\")\n if ((numFlags & FetchFlags.OBJECT_ID) === 0) {\n throw new ProtocolViolationError(\n `First fetch object cannot inherit Object ID (OBJECT_ID flag must be set)`,\n );\n }\n\n // PRIORITY must be present (absent = \"prior Object's Priority\" per Table 6)\n if ((numFlags & FetchFlags.PRIORITY) === 0) {\n throw new ProtocolViolationError(\n `First fetch object cannot inherit Priority (PRIORITY flag must be set)`,\n );\n }\n\n // Subgroup mode 1 or 2 requires prior object (unless DATAGRAM flag ignores subgroup)\n if (!isDatagram) {\n const subgroupMode = numFlags & FetchFlags.SUBGROUP_MODE_MASK;\n if (subgroupMode === FetchSubgroupMode.PRIOR || subgroupMode === FetchSubgroupMode.PRIOR_PLUS_ONE) {\n throw new ProtocolViolationError(\n `First fetch object cannot inherit Subgroup ID (subgroup mode ${subgroupMode} requires prior object)`,\n );\n }\n }\n}\n\n/**\n * Decode a single object from a fetch stream.\n * @param buf Buffer containing the object\n * @param offset Starting position\n * @param prior Context from prior object for inheritance (undefined for first object)\n * @param isFirstObject True if this is the first object in the fetch stream\n * @returns Decoded item (FetchObject or FetchEndOfRange) and bytes consumed\n * @throws {RangeError} If extension or payload length exceeds buffer\n * @throws {Error} If flags are invalid or first object tries to inherit\n * @see draft-ietf-moq-transport-16 §10.4.4\n */\nexport function decodeFetchObject(\n buf: Uint8Array,\n offset: number,\n prior: FetchPriorContext | undefined,\n isFirstObject: boolean,\n): { item: DecodedFetchItem; bytesRead: number } {\n let pos = offset;\n\n // Read serialization flags\n const { value: flags, bytesRead: flagsBytes } = readVarint(buf, pos);\n pos += flagsBytes;\n\n // Validate flags are in allowed range\n validateFetchFlags(flags);\n\n // Handle End of Range markers (§10.4.4.2)\n // Group ID and Object ID are present; Subgroup/Priority/Extensions are NOT present\n // Object Payload Length is still present per §10.4.4 (always present in Fetch objects)\n if (isEndOfRangeFlags(flags)) {\n // Read Group ID and Object ID\n const { value: groupId, bytesRead: gidBytes } = readVarint(buf, pos);\n pos += gidBytes;\n\n const { value: objectId, bytesRead: oidBytes } = readVarint(buf, pos);\n pos += oidBytes;\n\n // Read Object Payload Length (should be 0 for End of Range, but must be present)\n const { value: payloadLen, bytesRead: plBytes } = readVarint(buf, pos);\n pos += plBytes;\n\n // End of Range should have no payload - warn if non-zero but still consume it\n const numPayloadLen = Number(payloadLen);\n if (numPayloadLen > 0) {\n // Bounds check and skip payload if present\n if (pos + numPayloadLen > buf.length) {\n throw new RangeError(\n `End of Range payload length ${numPayloadLen} exceeds remaining buffer (${buf.length - pos} bytes)`,\n );\n }\n pos += numPayloadLen;\n }\n\n const endOfRange: FetchEndOfRange = {\n flags,\n groupId,\n objectId,\n nonExistent: flags === BigInt(FetchSpecialFlags.END_NON_EXISTENT),\n };\n\n return { item: endOfRange, bytesRead: pos - offset };\n }\n\n // Normal object - validate first object constraints\n if (isFirstObject) {\n validateFirstFetchObject(flags);\n } else if (prior === undefined) {\n // A non-first object inherits Group/Subgroup/Object/Priority from the prior\n // object; without one there is nothing to inherit. Fail cleanly rather than\n // dereferencing an undefined prior.\n throw new ProtocolViolationError('Non-first fetch object requires prior-object context');\n }\n\n const numFlags = Number(flags);\n const isDatagram = (numFlags & FetchFlags.DATAGRAM) !== 0;\n\n // Read Group ID (if present) or inherit\n let groupId: Varint;\n if ((numFlags & FetchFlags.GROUP_ID) !== 0) {\n const { value: gid, bytesRead: gidBytes } = readVarint(buf, pos);\n pos += gidBytes;\n groupId = gid;\n } else {\n // Inherit from prior (already validated not first object)\n groupId = varint(prior!.groupId);\n }\n\n // Determine subgroup ID based on mode\n let subgroupId: Varint;\n if (isDatagram) {\n // DATAGRAM flag: ignore subgroup bits, subgroup = 0\n subgroupId = varint(0);\n } else {\n const subgroupMode = numFlags & FetchFlags.SUBGROUP_MODE_MASK;\n switch (subgroupMode) {\n case FetchSubgroupMode.ZERO:\n subgroupId = varint(0);\n break;\n case FetchSubgroupMode.PRIOR:\n subgroupId = varint(prior!.subgroupId);\n break;\n case FetchSubgroupMode.PRIOR_PLUS_ONE:\n subgroupId = varint(prior!.subgroupId + 1n);\n break;\n case FetchSubgroupMode.EXPLICIT: {\n const { value: sgid, bytesRead: sgidBytes } = readVarint(buf, pos);\n pos += sgidBytes;\n subgroupId = sgid;\n break;\n }\n default:\n throw new ProtocolViolationError(`Invalid subgroup mode: ${subgroupMode}`);\n }\n }\n\n // Read Object ID (if present) or inherit + 1\n let objectId: Varint;\n if ((numFlags & FetchFlags.OBJECT_ID) !== 0) {\n const { value: oid, bytesRead: oidBytes } = readVarint(buf, pos);\n pos += oidBytes;\n objectId = oid;\n } else {\n // Inherit from prior + 1\n objectId = varint(prior!.objectId + 1n);\n }\n\n // Read Priority (if present) or inherit\n let publisherPriority: number | undefined;\n if ((numFlags & FetchFlags.PRIORITY) !== 0) {\n const { value: prio } = readUint8(buf, pos);\n pos += 1;\n publisherPriority = prio;\n } else if (prior?.priority !== undefined) {\n publisherPriority = prior.priority;\n }\n\n // Read Extensions (if present)\n let extensions: Uint8Array | undefined;\n if ((numFlags & FetchFlags.EXTENSIONS) !== 0) {\n const { value: extLen, bytesRead: extLenBytes } = readVarint(buf, pos);\n pos += extLenBytes;\n const extensionsLength = Number(extLen);\n\n // Bounds check\n if (pos + extensionsLength > buf.length) {\n throw new RangeError(\n `Extension length ${extensionsLength} exceeds remaining buffer (${buf.length - pos} bytes)`,\n );\n }\n\n extensions = buf.slice(pos, pos + extensionsLength);\n pos += extensionsLength;\n }\n\n // Read payload length and payload\n const { value: payloadLen, bytesRead: plBytes } = readVarint(buf, pos);\n pos += plBytes;\n const numPayloadLen = Number(payloadLen);\n\n // Bounds check for payload\n if (pos + numPayloadLen > buf.length) {\n throw new RangeError(\n `Payload length ${numPayloadLen} exceeds remaining buffer (${buf.length - pos} bytes)`,\n );\n }\n\n const payload = buf.slice(pos, pos + numPayloadLen);\n pos += numPayloadLen;\n\n const object: FetchObject = {\n flags,\n groupId,\n subgroupId,\n objectId,\n publisherPriority,\n isDatagram,\n extensions,\n payload,\n };\n\n return { item: object, bytesRead: pos - offset };\n}\n\n/**\n * Decode a single object from a draft-14 fetch stream.\n *\n * Draft-14 §10.4.4: All fields are always present (no serialization flags).\n * { Group ID (i), Subgroup ID (i), Object ID (i), Publisher Priority (8),\n * Extension Headers Length (i), [Extension headers (...)],\n * Object Payload Length (i), [Object Status (i)], Object Payload (..) }\n *\n * @see draft-ietf-moq-transport-14 §10.4.4\n */\nexport function decodeFetchObjectV14(\n buf: Uint8Array,\n offset: number,\n): { item: DecodedFetchItem; bytesRead: number } {\n let pos = offset;\n\n // Group ID\n const { value: groupId, bytesRead: gidBytes } = readVarint(buf, pos);\n pos += gidBytes;\n\n // Subgroup ID\n const { value: subgroupId, bytesRead: sgidBytes } = readVarint(buf, pos);\n pos += sgidBytes;\n\n // Object ID\n const { value: objectId, bytesRead: oidBytes } = readVarint(buf, pos);\n pos += oidBytes;\n\n // Publisher Priority (always present)\n const { value: publisherPriority } = readUint8(buf, pos);\n pos += 1;\n\n // Extension Headers Length\n const { value: extLen, bytesRead: extLenBytes } = readVarint(buf, pos);\n pos += extLenBytes;\n const extensionsLength = Number(extLen);\n\n let extensions: Uint8Array | undefined;\n if (extensionsLength > 0) {\n if (pos + extensionsLength > buf.length) {\n throw new RangeError(\n `Extension length ${extensionsLength} exceeds remaining buffer (${buf.length - pos} bytes)`,\n );\n }\n extensions = buf.slice(pos, pos + extensionsLength);\n pos += extensionsLength;\n }\n\n // Object Payload Length\n const { value: payloadLen, bytesRead: plBytes } = readVarint(buf, pos);\n pos += plBytes;\n const numPayloadLen = Number(payloadLen);\n\n if (numPayloadLen === 0) {\n // Status object — read status varint and return as FetchEndOfRange\n // so the adapter emits it as a gap signal.\n // @see draft-ietf-moq-transport-14 §10.4.4\n const { value: status, bytesRead: stBytes } = readVarint(buf, pos);\n pos += stBytes;\n validateObjectStatus(status, 14);\n\n const endOfRange: FetchEndOfRange = {\n flags: varint(0n),\n groupId,\n objectId,\n nonExistent: status === ObjectStatus.OBJECT_DOES_NOT_EXIST,\n rawStatus: status,\n };\n return { item: endOfRange, bytesRead: pos - offset };\n }\n\n if (pos + numPayloadLen > buf.length) {\n throw new RangeError(\n `Payload length ${numPayloadLen} exceeds remaining buffer (${buf.length - pos} bytes)`,\n );\n }\n const payload = buf.slice(pos, pos + numPayloadLen);\n pos += numPayloadLen;\n\n const object: FetchObject = {\n flags: varint(0n), // No flags in draft-14\n groupId,\n subgroupId,\n objectId,\n publisherPriority,\n isDatagram: false,\n extensions,\n payload,\n };\n\n return { item: object, bytesRead: pos - offset };\n}\n\n/**\n * Decode an OBJECT_DATAGRAM.\n * @param buf Buffer containing the entire datagram\n * @param offset Starting position\n * @returns Decoded datagram and bytes consumed\n * @throws {Error} If type byte is invalid or has invalid flag combination\n * @see draft-ietf-moq-transport-16 §10.3.1\n */\nexport function decodeObjectDatagram(\n buf: Uint8Array,\n offset: number,\n version: 14 | 16 = 16,\n): { datagram: ObjectDatagram; bytesRead: number } {\n let pos = offset;\n\n // Read type byte (varint, but typically 1 byte)\n const { value: typeVarint, bytesRead: typeBytes } = readVarint(buf, pos);\n pos += typeBytes;\n const typeByte = Number(typeVarint);\n\n // Validate type byte\n if (!isDatagramType(typeByte, version)) {\n throw new ProtocolViolationError(\n `Invalid OBJECT_DATAGRAM type byte: 0x${typeByte.toString(16)}`,\n );\n }\n\n // Check for invalid flag combinations\n if (!isValidDatagramFlags(typeByte)) {\n throw new ProtocolViolationError(\n `Invalid OBJECT_DATAGRAM flag combination (STATUS + END_OF_GROUP): 0x${typeByte.toString(16)}`,\n );\n }\n\n // Read track alias\n const { value: trackAlias, bytesRead: taBytes } = readVarint(buf, pos);\n pos += taBytes;\n\n // Read group ID\n const { value: groupId, bytesRead: gidBytes } = readVarint(buf, pos);\n pos += gidBytes;\n\n // Read object ID (unless ZERO_OBJECT_ID flag set)\n let objectId: Varint;\n if ((typeByte & DatagramFlags.ZERO_OBJECT_ID) !== 0) {\n objectId = varint(1);\n } else {\n const { value: oid, bytesRead: oidBytes } = readVarint(buf, pos);\n pos += oidBytes;\n objectId = oid;\n }\n\n // Read publisher priority.\n // Draft-14 §10.3.1: Publisher Priority is always present.\n // Draft-16: only present when DEFAULT_PRIORITY bit is NOT set.\n let publisherPriority: number | undefined;\n if (version === 14 || (typeByte & DatagramFlags.DEFAULT_PRIORITY) === 0) {\n const { value: prio } = readUint8(buf, pos);\n pos += 1;\n publisherPriority = prio;\n }\n\n // Read extensions if EXTENSIONS flag set\n let extensions: Uint8Array | undefined;\n let extensionsLength = 0;\n if ((typeByte & DatagramFlags.EXTENSIONS) !== 0) {\n const { value: extLen, bytesRead: extLenBytes } = readVarint(buf, pos);\n pos += extLenBytes;\n extensionsLength = Number(extLen);\n\n // §10.3.1: \"If an endpoint receives a datagram with the EXTENSIONS\n // bit set and an Extension Headers Length of 0, it MUST close the\n // session with a PROTOCOL_VIOLATION\"\n if (extensionsLength === 0) {\n throw new ProtocolViolationError(\n 'OBJECT_DATAGRAM has EXTENSIONS flag set but Extension Headers Length is 0',\n );\n }\n\n // Bounds check\n if (pos + extensionsLength > buf.length) {\n throw new RangeError(\n `Extension length ${extensionsLength} exceeds remaining buffer (${buf.length - pos} bytes)`,\n );\n }\n\n extensions = buf.slice(pos, pos + extensionsLength);\n pos += extensionsLength;\n }\n\n // Extract END_OF_GROUP flag\n const isEndOfGroup = (typeByte & DatagramFlags.END_OF_GROUP) !== 0;\n\n // Read status or payload\n let payload: Uint8Array;\n let status: Varint | undefined;\n\n if ((typeByte & DatagramFlags.STATUS) !== 0) {\n // STATUS flag: read status varint, no payload\n const { value: st, bytesRead: stBytes } = readVarint(buf, pos);\n pos += stBytes;\n status = st;\n payload = new Uint8Array(0);\n\n // Validate status code (version-aware: draft-14 allows 0x1)\n validateObjectStatus(status, version);\n\n // §10.2.1.2: Extensions on non-Normal status objects is PROTOCOL_VIOLATION\n if (extensionsLength > 0 && status !== ObjectStatus.NORMAL) {\n throw new ProtocolViolationError(\n `Extensions present on status datagram (status=0x${status.toString(16)}) per §10.2.1.2`,\n );\n }\n\n // STATUS datagrams must not have trailing bytes\n if (pos < buf.length) {\n throw new ProtocolViolationError(\n `STATUS datagram has ${buf.length - pos} trailing bytes (expected none)`,\n );\n }\n } else {\n // No STATUS flag: rest of datagram is payload\n payload = buf.slice(pos);\n pos = buf.length;\n }\n\n const datagram: ObjectDatagram = {\n typeByte,\n trackAlias,\n groupId,\n objectId,\n publisherPriority,\n isEndOfGroup,\n extensions,\n payload,\n status,\n };\n\n return { datagram, bytesRead: pos - offset };\n}\n","/**\n * QlogTrace — collects qlog events and exports as JSON per [QLOG-MAIN].\n *\n * Usage:\n * ```\n * const trace = new QlogTrace('session-123');\n * trace.record(event); // add events\n * const json = trace.toJSON(); // export as spec-compliant object\n * console.log(trace.toString());// export as JSON string\n * ```\n *\n * @see draft-pardue-moq-qlog-moq-events-06 §1\n * @see [QLOG-MAIN] draft-ietf-quic-qlog-main-schema\n * @module\n */\n\nimport type {\n ControlMessage,\n ParameterValue,\n TrackProperties,\n} from '../control/messages.js';\nimport type { Location } from '../primitives/location.js';\nimport type { QlogEvent, QlogExtensionHeader } from './types.js';\nimport { serializeSeq, toContainedFile } from './file.js';\nimport type { QlogEventRecord, QlogFileContained, QlogTraceSpec } from './file.js';\n\n/**\n * Event schema URI for the MOQT namespace.\n *\n * Pinned to -06 because that is the revision `types.ts` and this serializer\n * implement. Do not advance it without an event-by-event audit.\n *\n * @see draft-pardue-moq-qlog-moq-events-06\n */\nexport const MOQT_EVENT_SCHEMA = 'urn:ietf:params:qlog:events:moqt-06';\n\n// ─── Exported Types ─────────────────────────────────────────────────\n\n/**\n * A single timestamped qlog event in the trace.\n * @see [QLOG-MAIN] §3.3\n */\nexport interface QlogTraceEvent {\n /** Milliseconds since trace start (relative time). */\n readonly time: number;\n /** Event name in \"moqt:<event_type>\" format. */\n readonly name: string;\n /** Event data. */\n readonly data: Record<string, unknown>;\n}\n\n/**\n * Complete qlog JSON output per [QLOG-MAIN].\n *\n * Schema URI: urn:ietf:params:qlog:events:moqt-06\n * @see [QLOG-MAIN] §3.1\n */\nexport interface QlogTraceJson {\n readonly qlog_version: '0.4';\n readonly qlog_format: 'JSON';\n readonly title?: string;\n readonly traces: readonly [QlogTraceEntry];\n}\n\n/** A single trace entry within the qlog output. */\nexport interface QlogTraceEntry {\n readonly common_fields: {\n readonly group_id: string;\n readonly protocol_type: readonly ['moqt'];\n };\n readonly vantage_point: {\n readonly type: 'client';\n };\n readonly events: readonly QlogTraceEvent[];\n}\n\n// ─── ControlMessage → qlog $MOQTControlMessage ─────────────────────\n\n/**\n * Convert a ControlMessage to the qlog $MOQTControlMessage format.\n *\n * Maps our UPPER_CASE type discriminants to the lowercase names\n * defined in draft-pardue-moq-qlog-moq-events-06 §5.6.\n *\n * Byte arrays (trackNamespace, trackName) are converted to the\n * MOQTByteString format: `{ value: string }` for UTF-8 decodable\n * strings, or `{ value_bytes: hexstring }` for raw bytes.\n *\n * @see draft-pardue-moq-qlog-moq-events-06 §5.6\n */\nfunction controlMessageToQlog(msg: ControlMessage): Record<string, unknown> {\n const result: Record<string, unknown> = {\n type: msg.type.toLowerCase(),\n };\n\n // Add common fields based on what's present on the message\n if ('requestId' in msg && msg.requestId !== undefined) {\n result.request_id = qlogUint64(msg.requestId);\n }\n if ('trackAlias' in msg && msg.trackAlias !== undefined) {\n result.track_alias = qlogUint64(msg.trackAlias);\n }\n if ('errorCode' in msg && msg.errorCode !== undefined) {\n result.error_code = qlogUint64(msg.errorCode);\n }\n if ('errorReason' in msg && msg.errorReason !== undefined) {\n result.reason = msg.errorReason;\n }\n if ('trackNamespace' in msg && msg.trackNamespace !== undefined) {\n result.track_namespace = (msg.trackNamespace as Uint8Array[]).map(byteStringToQlog);\n }\n if ('trackName' in msg && msg.trackName !== undefined) {\n result.track_name = byteStringToQlog(msg.trackName as Uint8Array);\n }\n if ('trackNamespaceSuffix' in msg && msg.trackNamespaceSuffix !== undefined) {\n result.track_namespace_suffix = (msg.trackNamespaceSuffix as Uint8Array[]).map(byteStringToQlog);\n }\n if ('trackNamespacePrefix' in msg && msg.trackNamespacePrefix !== undefined) {\n result.track_namespace_prefix = (msg.trackNamespacePrefix as Uint8Array[]).map(byteStringToQlog);\n }\n if ('newSessionUri' in msg && msg.newSessionUri !== undefined) {\n // RawInfo.payload_length counts wire bytes, not UTF-16 code units.\n result.new_session_uri = rawInfo(new TextEncoder().encode(msg.newSessionUri).length);\n }\n if ('statusCode' in msg && msg.statusCode !== undefined) {\n result.status_code = qlogUint64(msg.statusCode);\n }\n if ('streamCount' in msg && msg.streamCount !== undefined) {\n result.stream_count = qlogUint64(msg.streamCount);\n }\n if ('maxRequestId' in msg && msg.maxRequestId !== undefined) {\n result.request_id = qlogUint64(msg.maxRequestId);\n }\n if ('maximumRequestId' in msg && msg.maximumRequestId !== undefined) {\n result.maximum_request_id = qlogUint64(msg.maximumRequestId);\n }\n if ('existingRequestId' in msg && msg.existingRequestId !== undefined) {\n result.existing_request_id = qlogUint64(msg.existingRequestId);\n }\n if ('retryInterval' in msg && msg.retryInterval !== undefined) {\n result.retry_interval = qlogUint64(msg.retryInterval);\n }\n if ('endOfTrack' in msg && msg.endOfTrack !== undefined) {\n result.end_of_track = msg.endOfTrack;\n }\n\n // FETCH: -06 requires `fetch_type` and nests the variant-specific fields.\n if ('fetch' in msg && msg.fetch !== undefined) {\n const f = msg.fetch;\n result.fetch_type = FETCH_TYPE_NAMES[f.fetchType] ?? String(f.fetchType);\n if (f.fetchType === 0x1) {\n result.standalone_fetch = {\n track_namespace: f.trackNamespace.map(byteStringToQlog),\n track_name: byteStringToQlog(f.trackName),\n start_location: locationToQlog(f.startLocation),\n end_location: locationToQlog(f.endLocation),\n };\n } else {\n result.joining_fetch = {\n joining_request_id: qlogUint64(f.joiningRequestId),\n joining_start: qlogUint64(f.joiningStart),\n };\n }\n }\n if ('endLocation' in msg && msg.endLocation !== undefined) {\n result.end_location = locationToQlog(msg.endLocation);\n }\n\n // draft-18 Track Properties map onto -06 `track_extensions`.\n const properties = ('trackProperties' in msg ? msg.trackProperties : undefined)\n ?? ('trackExtensions' in msg ? msg.trackExtensions : undefined);\n if (properties !== undefined) {\n result.track_extensions = trackPropertiesToQlog(properties);\n }\n\n // Fields our transport drafts carry that -06 does not define. Emitting them\n // under standardized-looking names would misrepresent the message, and\n // dropping them would lose diagnostics, so they are kept and marked.\n const unmapped: Record<string, unknown> = {};\n if ('requestKind' in msg && msg.requestKind !== undefined) {\n unmapped.request_kind = String(msg.requestKind);\n }\n if ('subscribeOptions' in msg && msg.subscribeOptions !== undefined) {\n const name = SUBSCRIBE_OPTIONS_NAMES.get(msg.subscribeOptions);\n if (name !== undefined) {\n result.subscribe_options = name;\n } else {\n unmapped.subscribe_options = qlogUint64(msg.subscribeOptions);\n }\n }\n if ('timeout' in msg && msg.timeout !== undefined) {\n unmapped.timeout = qlogUint64(msg.timeout as bigint);\n }\n if ('redirect' in msg && msg.redirect !== undefined) {\n // draft-18 §10.6.2 Redirect has no -06 representation. Keep its detail: a\n // bare boolean would drop the target the operator needs to follow.\n const r = msg.redirect;\n unmapped.redirect = {\n connect_uri: rawInfo(r.connectUri.length),\n track_namespace: r.trackNamespace.map(byteStringToQlog),\n track_name: byteStringToQlog(r.trackName),\n };\n }\n if (Object.keys(unmapped).length > 0) {\n result.playa_unmapped = unmapped;\n }\n\n // Parameter-bearing messages require `number_of_parameters` in -06; setup\n // messages name the list `setup_parameters`, everything else `parameters`.\n if ('parameters' in msg && msg.parameters !== undefined) {\n const setup = isSetup(msg);\n const entries = parametersToQlog(msg.parameters, setup ? 'setup' : 'message');\n result.number_of_parameters = entries.length;\n result[setup ? 'setup_parameters' : 'parameters'] = entries;\n } else if ('setupOptions' in msg && msg.setupOptions !== undefined) {\n // draft-18 SETUP carries Setup Options rather than a parameter map.\n const entries = parametersToQlog(msg.setupOptions, 'setup');\n result.number_of_parameters = entries.length;\n result.setup_parameters = entries;\n }\n\n return conformOrPrivatize(result);\n}\n\n/**\n * The concrete shape of every standardized -06 control message.\n *\n * Derived field-by-field from the CDDL in §5.7 (`required` omits the `type`\n * discriminant itself). Checking both directions makes the policy fail closed:\n * a message is private if it lacks a required field *or* carries one the\n * concrete definition does not have. The latter matters because our transport\n * union spans drafts 14, 16, and 18 — a newer-draft message can have a\n * recognized discriminant and still not be a faithful instance of the -06\n * alternative of that name.\n *\n * @see draft-pardue-moq-qlog-moq-events-06 §5.7\n */\nconst MOQT06_SHAPES: ReadonlyMap<string, { required: readonly string[]; allowed: readonly string[] }> =\n new Map([\n ['client_setup', {\n required: ['number_of_parameters'],\n allowed: ['number_of_parameters', 'setup_parameters'],\n }],\n ['server_setup', {\n required: ['number_of_parameters'],\n allowed: ['number_of_parameters', 'setup_parameters'],\n }],\n ['goaway', {\n required: ['new_session_uri'],\n allowed: ['new_session_uri'],\n }],\n ['max_request_id', {\n required: ['request_id'],\n allowed: ['request_id'],\n }],\n ['requests_blocked', {\n required: ['maximum_request_id'],\n allowed: ['maximum_request_id'],\n }],\n ['request_ok', {\n required: ['request_id', 'number_of_parameters'],\n allowed: ['request_id', 'number_of_parameters', 'parameters'],\n }],\n ['request_error', {\n required: ['request_id', 'error_code', 'retry_interval'],\n allowed: ['request_id', 'error_code', 'retry_interval', 'reason', 'reason_bytes'],\n }],\n ['subscribe', {\n required: ['request_id', 'track_namespace', 'track_name', 'number_of_parameters'],\n allowed: ['request_id', 'track_namespace', 'track_name', 'number_of_parameters', 'parameters'],\n }],\n ['subscribe_ok', {\n required: ['request_id', 'track_alias', 'number_of_parameters'],\n allowed: ['request_id', 'track_alias', 'number_of_parameters', 'parameters', 'track_extensions'],\n }],\n ['request_update', {\n required: ['request_id', 'existing_request_id', 'number_of_parameters'],\n allowed: ['request_id', 'existing_request_id', 'number_of_parameters', 'parameters'],\n }],\n ['unsubscribe', {\n required: ['request_id'],\n allowed: ['request_id'],\n }],\n ['publish', {\n required: ['request_id', 'track_namespace', 'track_name', 'track_alias', 'number_of_parameters'],\n allowed: ['request_id', 'track_namespace', 'track_name', 'track_alias', 'number_of_parameters', 'parameters', 'track_extensions'],\n }],\n ['publish_ok', {\n required: ['request_id', 'number_of_parameters'],\n allowed: ['request_id', 'number_of_parameters', 'parameters'],\n }],\n ['publish_done', {\n required: ['request_id', 'status_code', 'stream_count'],\n allowed: ['request_id', 'status_code', 'stream_count', 'reason', 'reason_bytes'],\n }],\n ['fetch', {\n required: ['request_id', 'fetch_type', 'number_of_parameters'],\n allowed: ['request_id', 'fetch_type', 'number_of_parameters', 'standalone_fetch', 'joining_fetch', 'parameters'],\n }],\n ['fetch_ok', {\n required: ['request_id', 'end_of_track', 'end_location', 'number_of_parameters'],\n allowed: ['request_id', 'end_of_track', 'end_location', 'number_of_parameters', 'parameters', 'track_extensions'],\n }],\n ['fetch_cancel', {\n required: ['request_id'],\n allowed: ['request_id'],\n }],\n ['track_status', {\n required: ['request_id', 'track_namespace', 'track_name', 'number_of_parameters'],\n allowed: ['request_id', 'track_namespace', 'track_name', 'number_of_parameters', 'parameters'],\n }],\n ['publish_namespace', {\n required: ['request_id', 'track_namespace', 'number_of_parameters'],\n allowed: ['request_id', 'track_namespace', 'number_of_parameters', 'parameters'],\n }],\n ['namespace', {\n required: ['track_namespace_suffix'],\n allowed: ['track_namespace_suffix'],\n }],\n ['publish_namespace_done', {\n required: ['request_id'],\n allowed: ['request_id'],\n }],\n ['namespace_done', {\n required: ['track_namespace_suffix'],\n allowed: ['track_namespace_suffix'],\n }],\n ['publish_namespace_cancel', {\n required: ['request_id', 'error_code'],\n allowed: ['request_id', 'error_code', 'reason', 'reason_bytes'],\n }],\n ['subscribe_namespace', {\n required: ['request_id', 'track_namespace_prefix', 'subscribe_options', 'number_of_parameters'],\n allowed: ['request_id', 'track_namespace_prefix', 'subscribe_options', 'number_of_parameters', 'parameters'],\n }],\n]);\n\n/**\n * Keep standardized type names honest.\n *\n * We support transport drafts 14, 16, and 18 while -06 tracks a single\n * revision, so a semantically valid message can lack a field the standardized\n * definition of the same name requires — draft-14 `PUBLISH_NAMESPACE_DONE`\n * carries a Track Namespace where -06 requires a Request ID. Presenting that\n * as the recognized structure would misrepresent it, and dropping it would\n * lose the diagnostic, so the type is marked private and the reason recorded.\n */\nfunction conformOrPrivatize(result: Record<string, unknown>): Record<string, unknown> {\n const type = result.type as string;\n\n const shape = MOQT06_SHAPES.get(type);\n if (shape === undefined) {\n return { ...result, type: `playa_${type}`, playa_reason: 'not defined by moqt-06' };\n }\n\n const present = Object.keys(result).filter(k => k !== 'type' && result[k] !== undefined);\n const missing = shape.required.filter(field => result[field] === undefined);\n // Name the fields that actually caused this, not the container they sit in.\n const extra = present\n .filter(field => !shape.allowed.includes(field))\n .flatMap(field =>\n field === 'playa_unmapped'\n ? Object.keys(result.playa_unmapped as Record<string, unknown>)\n : [field],\n );\n\n if (missing.length === 0 && extra.length === 0) return result;\n\n const reason = [\n missing.length > 0 ? `missing moqt-06 required field(s): ${missing.join(', ')}` : '',\n extra.length > 0 ? `field(s) outside the moqt-06 shape: ${extra.join(', ')}` : '',\n ].filter(Boolean).join('; ');\n\n return { ...result, type: `playa_${type}`, playa_reason: reason };\n}\n\n/**\n * Subscribe Options values to their -06 `$MOQTSubscribeOptions` names.\n *\n * Values per transport draft-16 §9.25: PUBLISH = 0x00, NAMESPACE = 0x01,\n * both = 0x02. An unrecognized value falls through to `playa_unmapped`.\n *\n * @see draft-pardue-moq-qlog-moq-events-06 §5.7.24\n */\nconst SUBSCRIBE_OPTIONS_NAMES: ReadonlyMap<bigint, string> = new Map([\n [0x00n, 'publish'],\n [0x01n, 'namespace'],\n [0x02n, 'both'],\n]);\n\n/** Fetch Type code to the -06 `$MOQTFetchType` name. */\nconst FETCH_TYPE_NAMES: Record<number, string> = {\n 0x1: 'standalone',\n 0x2: 'relative_joining',\n 0x3: 'absolute_joining',\n};\n\n/** Convert a Location to the -06 `MOQTLocation` form. */\nfunction locationToQlog(loc: Location): Record<string, unknown> {\n return { group: qlogUint64(loc.group), object: qlogUint64(loc.object) };\n}\n\n/**\n * Convert draft-18 Track Properties to -06 `track_extensions`.\n *\n * Even Property Types carry an integer (`header_value`); odd Types carry bytes\n * (`payload`).\n *\n * @see draft-pardue-moq-qlog-moq-events-06 §5.7 (MOQTExtensionHeader)\n */\nfunction trackPropertiesToQlog(props: TrackProperties): Record<string, unknown>[] {\n const out: Record<string, unknown>[] = [];\n for (const [type, values] of props) {\n for (const value of values) {\n out.push(\n value instanceof Uint8Array\n ? { header_type: qlogUint64(type), payload: { payload_length: value.length } }\n : { header_type: qlogUint64(type), header_value: qlogUint64(value) },\n );\n }\n }\n return out;\n}\n\n/** True for the messages whose parameter list -06 names `setup_parameters`. */\nfunction isSetup(msg: ControlMessage): boolean {\n return msg.type === 'CLIENT_SETUP' || msg.type === 'SERVER_SETUP' || msg.type === 'SETUP';\n}\n\n/**\n * Setup parameter types to their -06 names.\n *\n * The wire type is the protocol registry key, so this is a lookup, not an\n * inference. Types whose -06 shape we cannot faithfully produce (notably\n * `authorization_token`, whose definition requires an `alias_type` we do not\n * decode) are deliberately absent and fall through to the `unknown` form.\n *\n * @see draft-pardue-moq-qlog-moq-events-06 §5.4\n */\nconst SETUP_PARAM_NAMES: ReadonlyMap<bigint, { name: string; text?: boolean }> = new Map([\n [0x01n, { name: 'path', text: true }],\n [0x02n, { name: 'max_request_id' }],\n [0x04n, { name: 'max_auth_token_cache_size' }],\n [0x05n, { name: 'authority', text: true }],\n [0x07n, { name: 'implementation', text: true }],\n]);\n\n/**\n * Message parameter types to their -06 names.\n *\n * Absent by design: `authorization_token` (0x03) and `subscription_filter`\n * (0x21) have structured -06 values we cannot produce without more negotiated\n * context, and `rendezvous_timeout` (0x04), `subgroup_delivery_timeout`\n * (0x06), `fill_timeout` (0x0a), and `track_namespace_prefix` (0x34) have no\n * -06 definition at all. Those use the `unknown` form. Known scalars are not\n * relabelled unknown merely because their neighbours need more context.\n *\n * @see draft-pardue-moq-qlog-moq-events-06 §5.5\n */\nconst MESSAGE_PARAM_NAMES: ReadonlyMap<bigint, { name: string; location?: boolean }> = new Map([\n [0x02n, { name: 'delivery_timeout' }],\n [0x08n, { name: 'expires' }],\n [0x09n, { name: 'largest_object', location: true }],\n [0x10n, { name: 'forward' }],\n [0x20n, { name: 'subscriber_priority' }],\n [0x22n, { name: 'group_order' }],\n [0x32n, { name: 'new_group_request' }],\n]);\n\n/** RawInfo for a byte string we do not reproduce verbatim. */\nfunction rawInfo(byteLength: number): Record<string, unknown> {\n return { length: byteLength, payload_length: byteLength };\n}\n\n/** True for a Location-shaped parameter value. */\nfunction isLocation(v: ParameterValue): v is Location {\n return typeof v === 'object' && v !== null && !ArrayBuffer.isView(v) && !Array.isArray(v);\n}\n\n/**\n * Convert one parameter value to the `unknown` parameter form.\n *\n * `$MOQTUnknownParameter` carries `name_bytes` plus an optional scalar `value`\n * or `value_bytes` RawInfo. Values it cannot express — a Location or a Track\n * Namespace — are additionally carried under a clearly private `playa_value`\n * so the diagnostic is not lost while the standard fields stay correct.\n */\nfunction unknownParameter(type: bigint, value: ParameterValue): Record<string, unknown> {\n const out: Record<string, unknown> = { name: 'unknown', name_bytes: qlogUint64(type) };\n if (typeof value === 'bigint') {\n out.value = qlogUint64(value);\n } else if (value instanceof Uint8Array) {\n out.value_bytes = rawInfo(value.length);\n } else if (isLocation(value)) {\n out.playa_value = locationToQlog(value);\n } else {\n // NamespaceTuple — a list of namespace fields.\n const fields = value as readonly Uint8Array[];\n out.playa_value = { track_namespace: fields.map(byteStringToQlog) };\n }\n return out;\n}\n\n/**\n * Convert a parameter or Setup Option map to qlog `$MOQTParameter` entries.\n *\n * Recognized types use their registered -06 name and value shape; everything\n * else uses the specified `unknown` form rather than a plausible-looking\n * invention. A type permitting repeated values contributes one entry per\n * value, so the emitted count matches the wire.\n *\n * @see draft-pardue-moq-qlog-moq-events-06 §5.4, §5.5\n */\nfunction parametersToQlog(\n params: ReadonlyMap<bigint, readonly ParameterValue[]>,\n kind: 'setup' | 'message',\n): Record<string, unknown>[] {\n const out: Record<string, unknown>[] = [];\n for (const [type, values] of params) {\n for (const value of values) {\n out.push(convertParameter(type, value, kind));\n }\n }\n return out;\n}\n\nfunction convertParameter(\n type: bigint,\n value: ParameterValue,\n kind: 'setup' | 'message',\n): Record<string, unknown> {\n if (kind === 'setup') {\n const known = SETUP_PARAM_NAMES.get(type);\n if (known === undefined) return unknownParameter(type, value);\n if (known.text) {\n if (!(value instanceof Uint8Array)) return unknownParameter(type, value);\n // qlog is emitted before semantic session handling, so malformed bytes\n // can reach here. A lossy decode would silently change the observed\n // bytes and claim a valid standard text value.\n try {\n const text = new TextDecoder('utf-8', { fatal: true }).decode(value);\n return { name: known.name, value: text };\n } catch {\n return unknownParameter(type, value);\n }\n }\n if (typeof value !== 'bigint') return unknownParameter(type, value);\n return { name: known.name, value: qlogUint64(value) };\n }\n\n const known = MESSAGE_PARAM_NAMES.get(type);\n if (known === undefined) return unknownParameter(type, value);\n if (known.location) {\n if (!isLocation(value)) return unknownParameter(type, value);\n return { name: known.name, value: locationToQlog(value) };\n }\n if (typeof value !== 'bigint') return unknownParameter(type, value);\n return { name: known.name, value: qlogUint64(value) };\n}\n\n/**\n * Convert a Uint8Array to qlog MOQTByteString format (§5.4).\n *\n * Attempts UTF-8 decode; falls back to hex if invalid.\n * @see draft-pardue-moq-qlog-moq-events-06 §5.4\n */\nfunction byteStringToQlog(bytes: Uint8Array): { value?: string; value_bytes?: string } {\n try {\n const value = new TextDecoder('utf-8', { fatal: true }).decode(bytes);\n return { value };\n } catch {\n return { value_bytes: bytesToHex(bytes) };\n }\n}\n\n/** Convert a Uint8Array to a hex string. */\nfunction bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');\n}\n\nconst MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);\n\n/**\n * Serialize a uint64 for JSON output.\n *\n * Values through `Number.MAX_SAFE_INTEGER` are emitted as JSON numbers.\n * Larger values cannot round-trip through a double, so they are emitted as\n * decimal strings; parsers accept either form.\n *\n * @see [QLOG-MAIN] §11.3 (uint64 and I-JSON)\n */\nfunction qlogUint64(v: bigint | number): number | string {\n if (typeof v === 'number') return v;\n return v > MAX_SAFE_BIGINT ? v.toString(10) : Number(v);\n}\n\n// ─── Event Serialization ────────────────────────────────────────────\n\n/**\n * Convert a QlogEvent to its JSON-serializable data representation.\n *\n * - bigint → number, or decimal string above 2^53-1 (JSON has no bigint)\n * - ControlMessage → $MOQTControlMessage format\n * - Extension headers → qlog format\n */\nfunction eventToData(event: QlogEvent): Record<string, unknown> {\n switch (event.type) {\n case 'control_message_created':\n case 'control_message_parsed': {\n const data: Record<string, unknown> = {\n stream_id: qlogUint64(event.stream_id),\n message: controlMessageToQlog(event.message),\n };\n if (event.length !== undefined) data.length = event.length;\n if (event.raw !== undefined) data.raw = event.raw;\n return data;\n }\n\n case 'stream_type_set': {\n const data: Record<string, unknown> = {\n stream_id: qlogUint64(event.stream_id),\n stream_type: event.stream_type,\n };\n if (event.owner !== undefined) data.owner = event.owner;\n return data;\n }\n\n case 'object_datagram_parsed': {\n const data: Record<string, unknown> = {\n track_alias: qlogUint64(event.track_alias),\n group_id: qlogUint64(event.group_id),\n end_of_group: event.end_of_group,\n };\n // -06 §4.5: publisher_priority optional (inherits from subscription)\n if (event.publisher_priority !== undefined) data.publisher_priority = event.publisher_priority;\n if (event.object_id !== undefined) data.object_id = qlogUint64(event.object_id);\n if (event.extension_headers_length !== undefined) data.extension_headers_length = event.extension_headers_length;\n if (event.extension_headers !== undefined) data.extension_headers = event.extension_headers.map(extensionHeaderToJson);\n if (event.object_status !== undefined) data.object_status = qlogUint64(event.object_status);\n if (event.object_payload !== undefined) data.object_payload = event.object_payload;\n return data;\n }\n\n case 'subgroup_header_parsed': {\n // -06 §4.7: subgroup_id_mode required, publisher_priority optional\n const data: Record<string, unknown> = {\n stream_id: qlogUint64(event.stream_id),\n track_alias: qlogUint64(event.track_alias),\n group_id: qlogUint64(event.group_id),\n subgroup_id_mode: event.subgroup_id_mode,\n contains_end_of_group: event.contains_end_of_group,\n extensions_present: event.extensions_present,\n };\n if (event.subgroup_id !== undefined) data.subgroup_id = qlogUint64(event.subgroup_id);\n if (event.publisher_priority !== undefined) data.publisher_priority = event.publisher_priority;\n return data;\n }\n\n case 'subgroup_object_parsed': {\n // -06 §4.9: object_id_delta replaces object_id; group_id, subgroup_id,\n // extension_headers_length removed\n const data: Record<string, unknown> = {\n stream_id: qlogUint64(event.stream_id),\n object_id_delta: qlogUint64(event.object_id_delta),\n object_payload_length: event.object_payload_length,\n };\n if (event.extension_headers !== undefined) data.extension_headers = event.extension_headers.map(extensionHeaderToJson);\n if (event.object_status !== undefined) data.object_status = qlogUint64(event.object_status);\n if (event.object_payload !== undefined) data.object_payload = event.object_payload;\n return data;\n }\n\n case 'fetch_header_parsed': {\n return {\n stream_id: qlogUint64(event.stream_id),\n request_id: qlogUint64(event.request_id),\n };\n }\n\n case 'fetch_object_parsed': {\n // -06 §4.13: datagram, end_of_nonexistent_range, end_of_unknown_range required;\n // group_id, subgroup_id, object_id, publisher_priority, extension_headers_length optional\n const data: Record<string, unknown> = {\n stream_id: qlogUint64(event.stream_id),\n datagram: event.datagram,\n end_of_nonexistent_range: event.end_of_nonexistent_range,\n end_of_unknown_range: event.end_of_unknown_range,\n object_payload_length: event.object_payload_length,\n };\n if (event.subgroup_id_bits !== undefined) data.subgroup_id_bits = event.subgroup_id_bits;\n if (event.group_id !== undefined) data.group_id = qlogUint64(event.group_id);\n if (event.subgroup_id !== undefined) data.subgroup_id = qlogUint64(event.subgroup_id);\n if (event.object_id !== undefined) data.object_id = qlogUint64(event.object_id);\n if (event.publisher_priority !== undefined) data.publisher_priority = event.publisher_priority;\n if (event.extension_headers_length !== undefined) data.extension_headers_length = event.extension_headers_length;\n if (event.extension_headers !== undefined) data.extension_headers = event.extension_headers.map(extensionHeaderToJson);\n if (event.object_status !== undefined) data.object_status = qlogUint64(event.object_status);\n if (event.object_payload !== undefined) data.object_payload = event.object_payload;\n return data;\n }\n }\n}\n\n/** Convert a QlogExtensionHeader to JSON-serializable format. */\nfunction extensionHeaderToJson(h: QlogExtensionHeader): Record<string, unknown> {\n const result: Record<string, unknown> = {\n header_type: qlogUint64(h.header_type),\n };\n if (h.header_value !== undefined) result.header_value = qlogUint64(h.header_value);\n if (h.header_length !== undefined) result.header_length = qlogUint64(h.header_length);\n if (h.payload !== undefined) result.payload = h.payload;\n return result;\n}\n\n// ─── QlogTrace ──────────────────────────────────────────────────────\n\n/**\n * Collects qlog events and exports as JSON per [QLOG-MAIN].\n *\n * Timestamps are relative to trace construction time using an\n * injectable clock (defaults to `performance.now()`).\n *\n * @see draft-pardue-moq-qlog-moq-events-06 §1\n * @see [QLOG-MAIN] draft-ietf-quic-qlog-main-schema\n */\nexport class QlogTrace {\n private readonly events: QlogTraceEvent[] = [];\n private readonly startTime: number;\n\n /**\n * @param sessionId Globally unique session identifier (used as group_id).\n * @param clock Injectable time source returning milliseconds. Defaults to performance.now().\n */\n constructor(\n private readonly sessionId: string,\n private readonly clock: () => number = () => performance.now(),\n ) {\n this.startTime = this.clock();\n }\n\n /**\n * Record a qlog event with current timestamp.\n *\n * The event is timestamped relative to trace start and stored\n * for later export via toJSON()/toString().\n */\n record(event: QlogEvent): void {\n const time = this.clock() - this.startTime;\n this.events.push({\n time,\n name: `moqt:${event.type}`,\n data: eventToData(event),\n });\n }\n\n /**\n * Export in the legacy qlog 0.4 shape.\n *\n * Retained unchanged for existing consumers. That shape predates the\n * current schema — it carries `qlog_version`/`protocol_type` where draft-14\n * expects `file_schema`, `serialization_format`, and a per-trace\n * `event_schemas` array — so new code should use {@link toContained} or\n * {@link toSeq}.\n *\n * @deprecated Use {@link toContained} (`.qlog`) or {@link toSeq} (`.sqlog`).\n * @see [QLOG-MAIN] §3.1\n */\n toJSON(): QlogTraceJson {\n return {\n qlog_version: '0.4',\n qlog_format: 'JSON',\n traces: [{\n common_fields: {\n group_id: this.sessionId,\n protocol_type: ['moqt'] as const,\n },\n vantage_point: {\n type: 'client' as const,\n },\n events: [...this.events],\n }],\n };\n }\n\n /**\n * Export as JSON string in the legacy 0.4 shape.\n *\n * Uses 2-space indentation for human readability.\n *\n * @deprecated Use {@link toContained} or {@link toSeq}.\n */\n toString(): string {\n return JSON.stringify(this.toJSON(), null, 2);\n }\n\n /**\n * Trace declaration for the current schema.\n *\n * The session identifier is the trace-wide `group_id`, per MOQT qlog -06\n * §1.1, so events from separate sessions stay distinguishable when several\n * are logged together.\n */\n traceSpec(): QlogTraceSpec {\n return {\n event_schemas: [MOQT_EVENT_SCHEMA],\n vantage_point: { type: 'client' },\n common_fields: {\n group_id: this.sessionId,\n time_format: 'relative_to_epoch',\n reference_time: { clock_type: 'monotonic', epoch: 'unknown' },\n },\n };\n }\n\n /** Recorded events as generic qlog records. */\n toRecords(): readonly QlogEventRecord[] {\n return this.events.map(e => ({ time: e.time, name: e.name, data: e.data }));\n }\n\n /**\n * Export as a contained qlog file (`.qlog`) per draft-14.\n *\n * @see draft-ietf-quic-qlog-main-schema-14\n */\n toContained(): QlogFileContained {\n return toContainedFile(this.traceSpec(), this.toRecords());\n }\n\n /**\n * Export as a sequential qlog file (`.sqlog`) per draft-14 and RFC 7464.\n *\n * Every record is RS-prefixed and LF-terminated, so the output appends\n * cleanly and a capture cut short still parses up to the cut.\n */\n toSeq(): string {\n return serializeSeq(this.traceSpec(), this.toRecords());\n }\n\n /** Number of recorded events. */\n get length(): number {\n return this.events.length;\n }\n\n /** Clear all recorded events. Timestamps continue from original start time. */\n clear(): void {\n this.events.length = 0;\n }\n}\n","/**\n * AUTHORIZATION_TOKEN structure parsing and encoding.\n *\n * Parses the Token structure from raw KVP parameter bytes into a typed\n * discriminated union based on the Alias Type field.\n *\n * @see draft-ietf-moq-transport-16 §9.2.2.1, Figure 4\n * @module\n */\n\nimport { varint, readVarint, writeVarint, varintEncodingLength } from '../primitives/varint.js';\nimport { readVi64, writeVi64, vi64EncodingLength } from '../primitives/vi64.js';\nimport { ProtocolViolationError } from '../errors.js';\n\n// ─── Alias Type Constants ─────────────────────────────────────────────\n\n/**\n * Authorization Token Alias Types.\n * @see draft-ietf-moq-transport-16 §9.2.2.1, §13.1\n */\nexport const AliasType = {\n /** Retire a previously registered alias. Has Alias, no Type/Value. */\n DELETE: varint(0x0),\n /** Associate an alias with a Token Type + Value. Has Alias, Type, Value. */\n REGISTER: varint(0x1),\n /** Reference a previously registered token by alias. Has Alias, no Type/Value. */\n USE_ALIAS: varint(0x2),\n /** Provide Token Type + Value directly (no caching). No Alias, has Type/Value. */\n USE_VALUE: varint(0x3),\n} as const;\n\n// ─── Token Types ──────────────────────────────────────────────────────\n\n/**\n * DELETE token: retire a previously registered alias.\n * @see draft-ietf-moq-transport-16 §9.2.2.1 — DELETE (0x0)\n */\nexport interface DeleteToken {\n readonly aliasType: typeof AliasType.DELETE;\n /**\n * Token Alias — semantic value is a raw `bigint`. On draft-14/16 it is a QUIC\n * varint (≤ 2^62-1, enforced by the wire parser/encoder); on draft-18 it is a\n * vi64 (full uint64). See {@link parseAuthorizationToken18}.\n */\n readonly tokenAlias: bigint;\n}\n\n/**\n * REGISTER token: associate an alias with a Token Type + Value.\n * @see draft-ietf-moq-transport-16 §9.2.2.1 — REGISTER (0x1)\n */\nexport interface RegisterToken {\n readonly aliasType: typeof AliasType.REGISTER;\n /** Token Alias — see {@link DeleteToken.tokenAlias} (vi64 on draft-18). */\n readonly tokenAlias: bigint;\n /** Token Type — QUIC varint on draft-14/16, vi64 (full uint64) on draft-18. */\n readonly tokenType: bigint;\n readonly tokenValue: Uint8Array;\n}\n\n/**\n * USE_ALIAS token: reference a previously registered token by alias.\n * @see draft-ietf-moq-transport-16 §9.2.2.1 — USE_ALIAS (0x2)\n */\nexport interface UseAliasToken {\n readonly aliasType: typeof AliasType.USE_ALIAS;\n /** Token Alias — see {@link DeleteToken.tokenAlias} (vi64 on draft-18). */\n readonly tokenAlias: bigint;\n}\n\n/**\n * USE_VALUE token: provide Token Type + Value directly.\n * @see draft-ietf-moq-transport-16 §9.2.2.1 — USE_VALUE (0x3)\n */\nexport interface UseValueToken {\n readonly aliasType: typeof AliasType.USE_VALUE;\n /** Token Type — QUIC varint on draft-14/16, vi64 (full uint64) on draft-18. */\n readonly tokenType: bigint;\n readonly tokenValue: Uint8Array;\n}\n\n/**\n * Discriminated union of all Authorization Token variants.\n * @see draft-ietf-moq-transport-16 §9.2.2.1, Figure 4\n */\nexport type AuthorizationToken = DeleteToken | RegisterToken | UseAliasToken | UseValueToken;\n\n/**\n * A resolved token — the final (tokenType, tokenValue) pair after alias resolution.\n * This is the output of processing any token through the alias cache.\n */\nexport interface ResolvedToken {\n /** Token Type — vi64 (full uint64) on draft-18, QUIC varint on draft-14/16. */\n readonly tokenType: bigint;\n readonly tokenValue: Uint8Array;\n}\n\n// ─── Parsing ──────────────────────────────────────────────────────────\n\n/**\n * Parse an Authorization Token from raw KVP parameter bytes.\n *\n * The Token structure (Figure 4) is:\n * ```\n * Token {\n * Alias Type (i),\n * [Token Alias (i),]\n * [Token Type (i),]\n * [Token Value (..)]\n * }\n * ```\n *\n * Token Value consumes all remaining bytes after Token Type.\n *\n * @throws {Error} if the token is malformed (truncated, unknown alias type)\n * @see draft-ietf-moq-transport-16 §9.2.2.1, Figure 4\n */\nexport function parseAuthorizationToken(data: Uint8Array): AuthorizationToken {\n if (data.length === 0) {\n throw new ProtocolViolationError('Empty AUTHORIZATION_TOKEN parameter');\n }\n\n let pos = 0;\n\n // Read Alias Type\n const { value: aliasTypeRaw, bytesRead: atBytes } = readVarint(data, pos);\n pos += atBytes;\n\n switch (aliasTypeRaw) {\n case AliasType.DELETE as bigint: {\n // DELETE: has Token Alias, no Type/Value\n if (pos >= data.length) {\n throw new ProtocolViolationError('DELETE token missing Token Alias');\n }\n const { value: tokenAlias, bytesRead: taBytes } = readVarint(data, pos);\n pos += taBytes;\n if (pos !== data.length) {\n throw new ProtocolViolationError(`DELETE token has ${data.length - pos} trailing bytes`);\n }\n return { aliasType: AliasType.DELETE, tokenAlias };\n }\n\n case AliasType.REGISTER as bigint: {\n // REGISTER: has Token Alias, Token Type, Token Value\n if (pos >= data.length) {\n throw new ProtocolViolationError('REGISTER token missing Token Alias');\n }\n const { value: tokenAlias, bytesRead: taBytes } = readVarint(data, pos);\n pos += taBytes;\n if (pos >= data.length) {\n throw new ProtocolViolationError('REGISTER token missing Token Type');\n }\n const { value: tokenType, bytesRead: ttBytes } = readVarint(data, pos);\n pos += ttBytes;\n // Token Value is all remaining bytes (may be empty)\n const tokenValue = data.slice(pos);\n return { aliasType: AliasType.REGISTER, tokenAlias, tokenType, tokenValue };\n }\n\n case AliasType.USE_ALIAS as bigint: {\n // USE_ALIAS: has Token Alias, no Type/Value\n if (pos >= data.length) {\n throw new ProtocolViolationError('USE_ALIAS token missing Token Alias');\n }\n const { value: tokenAlias, bytesRead: taBytes } = readVarint(data, pos);\n pos += taBytes;\n if (pos !== data.length) {\n throw new ProtocolViolationError(`USE_ALIAS token has ${data.length - pos} trailing bytes`);\n }\n return { aliasType: AliasType.USE_ALIAS, tokenAlias };\n }\n\n case AliasType.USE_VALUE as bigint: {\n // USE_VALUE: no Token Alias, has Token Type, Token Value\n if (pos >= data.length) {\n throw new ProtocolViolationError('USE_VALUE token missing Token Type');\n }\n const { value: tokenType, bytesRead: ttBytes } = readVarint(data, pos);\n pos += ttBytes;\n // Token Value is all remaining bytes (may be empty)\n const tokenValue = data.slice(pos);\n return { aliasType: AliasType.USE_VALUE, tokenType, tokenValue };\n }\n\n default:\n throw new ProtocolViolationError(`Unknown AUTHORIZATION_TOKEN Alias Type: ${aliasTypeRaw}`);\n }\n}\n\n// ─── Encoding ─────────────────────────────────────────────────────────\n\n/**\n * Encode an Authorization Token to wire format bytes.\n * @see draft-ietf-moq-transport-16 §9.2.2.1, Figure 4\n */\nexport function encodeAuthorizationToken(token: AuthorizationToken): Uint8Array {\n switch (token.aliasType) {\n case AliasType.DELETE as bigint: {\n const t = token as DeleteToken;\n const size = varintEncodingLength(AliasType.DELETE) + varintEncodingLength(t.tokenAlias);\n const buf = new Uint8Array(size);\n let pos = writeVarint(AliasType.DELETE, buf, 0);\n writeVarint(t.tokenAlias, buf, pos);\n return buf;\n }\n\n case AliasType.REGISTER as bigint: {\n const t = token as RegisterToken;\n const size = varintEncodingLength(AliasType.REGISTER)\n + varintEncodingLength(t.tokenAlias)\n + varintEncodingLength(t.tokenType)\n + t.tokenValue.length;\n const buf = new Uint8Array(size);\n let pos = writeVarint(AliasType.REGISTER, buf, 0);\n pos += writeVarint(t.tokenAlias, buf, pos);\n pos += writeVarint(t.tokenType, buf, pos);\n buf.set(t.tokenValue, pos);\n return buf;\n }\n\n case AliasType.USE_ALIAS as bigint: {\n const t = token as UseAliasToken;\n const size = varintEncodingLength(AliasType.USE_ALIAS) + varintEncodingLength(t.tokenAlias);\n const buf = new Uint8Array(size);\n let pos = writeVarint(AliasType.USE_ALIAS, buf, 0);\n writeVarint(t.tokenAlias, buf, pos);\n return buf;\n }\n\n case AliasType.USE_VALUE as bigint: {\n const t = token as UseValueToken;\n const size = varintEncodingLength(AliasType.USE_VALUE)\n + varintEncodingLength(t.tokenType)\n + t.tokenValue.length;\n const buf = new Uint8Array(size);\n let pos = writeVarint(AliasType.USE_VALUE, buf, 0);\n pos += writeVarint(t.tokenType, buf, pos);\n buf.set(t.tokenValue, pos);\n return buf;\n }\n\n default:\n throw new ProtocolViolationError(`Unknown Alias Type: ${(token as AuthorizationToken).aliasType}`);\n }\n}\n\n// ─── draft-18 (vi64) ──────────────────────────────────────────────────\n//\n// draft-18 encodes the Token structure's Alias Type, Token Alias, and Token Type\n// as vi64 (full uint64), not the QUIC varint of draft-14/16. The wire bytes are\n// identical for small values; they differ only above the QUIC range (2^62-1).\n// The parse/encode helpers below mirror the legacy ones but with vi64, so a\n// draft-18 token alias / type can carry any uint64.\n//\n// The SEMANTIC token shape is shared: the legacy {@link AuthorizationToken} types\n// already carry `bigint` Token Alias / Token Type, so the `*18` names are aliases.\n// Only the WIRE codec differs (QUIC varint vs vi64). The Alias Type discriminant\n// values (0x0–0x3) are shared with draft-14/16.\n// @see draft-ietf-moq-transport-18 §10.2 (message parameters), §9.2.2.1 (Token)\n\n/** draft-18 DELETE token (vi64 wire); semantically identical to {@link DeleteToken}. */\nexport type DeleteToken18 = DeleteToken;\n/** draft-18 REGISTER token (vi64 wire); semantically identical to {@link RegisterToken}. */\nexport type RegisterToken18 = RegisterToken;\n/** draft-18 USE_ALIAS token (vi64 wire); semantically identical to {@link UseAliasToken}. */\nexport type UseAliasToken18 = UseAliasToken;\n/** draft-18 USE_VALUE token (vi64 wire); semantically identical to {@link UseValueToken}. */\nexport type UseValueToken18 = UseValueToken;\n/** draft-18 Authorization Token (vi64-encoded internals); alias of {@link AuthorizationToken}. */\nexport type AuthorizationToken18 = AuthorizationToken;\n\n/**\n * Parse a draft-18 Authorization Token from raw parameter bytes (vi64 internals).\n * @throws {ProtocolViolationError} on a malformed / truncated / unknown-alias token.\n * @see draft-ietf-moq-transport-18 §9.2.2.1\n */\nexport function parseAuthorizationToken18(data: Uint8Array): AuthorizationToken18 {\n if (data.length === 0) {\n throw new ProtocolViolationError('Empty AUTHORIZATION_TOKEN parameter');\n }\n let pos = 0;\n const { value: aliasTypeRaw, bytesRead: atBytes } = readVi64(data, pos);\n pos += atBytes;\n\n switch (aliasTypeRaw) {\n case AliasType.DELETE as bigint: {\n if (pos >= data.length) throw new ProtocolViolationError('DELETE token missing Token Alias');\n const { value: tokenAlias, bytesRead } = readVi64(data, pos); pos += bytesRead;\n if (pos !== data.length) throw new ProtocolViolationError(`DELETE token has ${data.length - pos} trailing bytes`);\n return { aliasType: AliasType.DELETE, tokenAlias };\n }\n case AliasType.REGISTER as bigint: {\n if (pos >= data.length) throw new ProtocolViolationError('REGISTER token missing Token Alias');\n const ta = readVi64(data, pos); pos += ta.bytesRead;\n if (pos >= data.length) throw new ProtocolViolationError('REGISTER token missing Token Type');\n const tt = readVi64(data, pos); pos += tt.bytesRead;\n return { aliasType: AliasType.REGISTER, tokenAlias: ta.value, tokenType: tt.value, tokenValue: data.slice(pos) };\n }\n case AliasType.USE_ALIAS as bigint: {\n if (pos >= data.length) throw new ProtocolViolationError('USE_ALIAS token missing Token Alias');\n const { value: tokenAlias, bytesRead } = readVi64(data, pos); pos += bytesRead;\n if (pos !== data.length) throw new ProtocolViolationError(`USE_ALIAS token has ${data.length - pos} trailing bytes`);\n return { aliasType: AliasType.USE_ALIAS, tokenAlias };\n }\n case AliasType.USE_VALUE as bigint: {\n if (pos >= data.length) throw new ProtocolViolationError('USE_VALUE token missing Token Type');\n const { value: tokenType, bytesRead } = readVi64(data, pos); pos += bytesRead;\n return { aliasType: AliasType.USE_VALUE, tokenType, tokenValue: data.slice(pos) };\n }\n default:\n throw new ProtocolViolationError(`Unknown AUTHORIZATION_TOKEN Alias Type: ${aliasTypeRaw}`);\n }\n}\n\n/**\n * Encode a draft-18 Authorization Token to wire bytes (vi64 internals).\n * @see draft-ietf-moq-transport-18 §9.2.2.1\n */\nexport function encodeAuthorizationToken18(token: AuthorizationToken18): Uint8Array {\n switch (token.aliasType) {\n case AliasType.DELETE as bigint: {\n const t = token as DeleteToken18;\n const buf = new Uint8Array(vi64EncodingLength(AliasType.DELETE) + vi64EncodingLength(t.tokenAlias));\n let p = writeVi64(AliasType.DELETE, buf, 0);\n writeVi64(t.tokenAlias, buf, p);\n return buf;\n }\n case AliasType.REGISTER as bigint: {\n const t = token as RegisterToken18;\n const buf = new Uint8Array(\n vi64EncodingLength(AliasType.REGISTER) + vi64EncodingLength(t.tokenAlias) + vi64EncodingLength(t.tokenType) + t.tokenValue.length,\n );\n let p = writeVi64(AliasType.REGISTER, buf, 0);\n p += writeVi64(t.tokenAlias, buf, p);\n p += writeVi64(t.tokenType, buf, p);\n buf.set(t.tokenValue, p);\n return buf;\n }\n case AliasType.USE_ALIAS as bigint: {\n const t = token as UseAliasToken18;\n const buf = new Uint8Array(vi64EncodingLength(AliasType.USE_ALIAS) + vi64EncodingLength(t.tokenAlias));\n let p = writeVi64(AliasType.USE_ALIAS, buf, 0);\n writeVi64(t.tokenAlias, buf, p);\n return buf;\n }\n case AliasType.USE_VALUE as bigint: {\n const t = token as UseValueToken18;\n const buf = new Uint8Array(\n vi64EncodingLength(AliasType.USE_VALUE) + vi64EncodingLength(t.tokenType) + t.tokenValue.length,\n );\n let p = writeVi64(AliasType.USE_VALUE, buf, 0);\n p += writeVi64(t.tokenType, buf, p);\n buf.set(t.tokenValue, p);\n return buf;\n }\n default:\n throw new ProtocolViolationError(`Unknown Alias Type: ${(token as AuthorizationToken18).aliasType}`);\n }\n}\n","/**\n * Setup handshake gating and parameter validation.\n *\n * Enforces that CLIENT_SETUP and SERVER_SETUP are the first messages\n * exchanged on the control stream. Validates setup parameters and\n * extracts MAX_REQUEST_ID for request ID allocation.\n *\n * @see draft-ietf-moq-transport-16 §9.3\n * @module\n */\n\nimport { varint, type Varint } from '../primitives/varint.js';\nimport type { ControlMessage, ClientSetup, ServerSetup, Parameters, Setup, SetupOptionMap } from '../control/messages.js';\nimport { SetupParam } from '../control/parameters.js';\nimport { SetupOption18, SetupOption22 } from '../control/codes-18.js';\nimport type { DraftVersion } from '../control/codec.js';\nimport { EndpointRole, type EndpointRoleValue, SessionState, type SessionStateValue } from './types.js';\nimport { AliasType, parseAuthorizationToken, parseAuthorizationToken18, type AuthorizationToken } from '../control/auth-token.js';\nimport { isDraft22, isRequestStreamDraft } from '../versions.js';\n\n/**\n * Error thrown for setup handshake violations.\n */\nexport class SetupError extends Error {\n constructor(\n message: string,\n public readonly code:\n | 'PROTOCOL_VIOLATION'\n | 'VERSION_NEGOTIATION_FAILED'\n | 'INVALID_PATH'\n | 'INVALID_AUTHORITY'\n | 'MALFORMED_PATH'\n | 'MALFORMED_AUTHORITY'\n | 'KEY_VALUE_FORMATTING_ERROR',\n ) {\n super(message);\n this.name = 'SetupError';\n }\n}\n\n/**\n * Result of successful setup handshake.\n */\nexport interface SetupResult {\n /** MAX_REQUEST_ID from peer's setup (default 0 if not provided). */\n readonly peerMaxRequestId: Varint;\n /** MOQT_IMPLEMENTATION from peer if provided. */\n readonly peerImplementation?: string;\n /** PATH from CLIENT_SETUP (server only). */\n readonly path?: string;\n /** AUTHORITY from CLIENT_SETUP (server only). */\n readonly authority?: string;\n /**\n * MAX_AUTH_TOKEN_CACHE_SIZE from peer's setup.\n * Indicates how many bytes of token aliases the peer is willing to cache.\n * Default 0 = aliases prohibited. Raw `bigint`: draft-18 §10.3.1.3 carries this\n * as a vi64 (full uint64); draft-14/16 use a QUIC varint (range-guarded there).\n * @see draft-ietf-moq-transport-18 §10.3.1.3\n */\n readonly peerMaxAuthTokenCacheSize?: bigint;\n /** draft-22 §9.1.7 MAX_REQUEST_UPDATES from the peer's SETUP (absent = 0, no limit). */\n readonly peerMaxRequestUpdates?: bigint;\n /**\n * Parsed AUTHORIZATION_TOKEN parameters from the peer's setup message.\n * These are the raw parsed tokens — caller must process through AuthTokenCache.\n * @see draft-ietf-moq-transport-16 §9.3.1.5\n */\n readonly authTokens?: AuthorizationToken[];\n}\n\n/**\n * Manages the setup handshake for a MOQT session.\n *\n * Setup flow:\n * - Client: Create CLIENT_SETUP → send → receive SERVER_SETUP → done\n * - Server: Receive CLIENT_SETUP → validate → create SERVER_SETUP → send → done\n *\n * @example\n * ```typescript\n * // Client side\n * const setup = new SetupGate('client');\n * const clientSetup = setup.createClientSetup({ maxRequestId: 100n });\n * // send clientSetup...\n * // receive serverSetup...\n * const result = setup.handleServerSetup(serverSetup);\n *\n * // Server side\n * const setup = new SetupGate('server');\n * // receive clientSetup...\n * const result = setup.handleClientSetup(clientSetup);\n * const serverSetup = setup.createServerSetup({ maxRequestId: 100n });\n * // send serverSetup...\n * ```\n */\n\n/**\n * Known draft-18 Setup Options that MUST NOT be repeated (§10.3). Every known\n * option except AUTHORIZATION_TOKEN (0x03, explicitly repeatable). Unknown\n * options may be duplicated and are ignored.\n */\nconst KNOWN_SINGLETON_SETUP_OPTIONS_18: ReadonlySet<number> = new Set([\n SetupOption18.PATH, // 0x01\n SetupOption18.MAX_AUTH_TOKEN_CACHE_SIZE, // 0x04\n SetupOption18.AUTHORITY, // 0x05\n SetupOption18.MOQT_IMPLEMENTATION, // 0x07\n]);\n\nexport class SetupGate {\n private state: SessionStateValue = SessionState.IDLE;\n private setupResult: SetupResult | undefined;\n\n constructor(\n private readonly role: EndpointRoleValue,\n private readonly draftVersion: DraftVersion = 16,\n /**\n * Whether the underlying transport is WebTransport. Per §10.3.1.1/§10.3.1.2,\n * PATH and AUTHORITY MUST NOT be used over WebTransport — a received PATH /\n * AUTHORITY closes the session with INVALID_PATH / INVALID_AUTHORITY. Default\n * `false` = native QUIC, where the legacy client→server PATH semantics apply.\n */\n private readonly webtransport: boolean = false,\n ) {}\n\n /**\n * Get the current session state.\n */\n get sessionState(): SessionStateValue {\n return this.state;\n }\n\n /**\n * Check if setup handshake is complete.\n */\n isComplete(): boolean {\n return this.state === SessionState.ESTABLISHED;\n }\n\n /**\n * Get the setup result after handshake completes.\n * @throws {Error} If handshake not complete\n */\n getResult(): SetupResult {\n if (!this.setupResult) {\n throw new Error('Setup handshake not complete');\n }\n return this.setupResult;\n }\n\n /**\n * Validate that the received message is appropriate for current state.\n * @throws {SetupError} If message violates handshake rules\n */\n validateMessage(msg: ControlMessage): void {\n if (isRequestStreamDraft(this.draftVersion)) {\n // draft-18: each side sends a single unified SETUP on its uni control\n // stream; the first message received before ESTABLISHED must be SETUP.\n if (this.state !== SessionState.ESTABLISHED && msg.type !== 'SETUP') {\n throw new SetupError(`draft-18 expects SETUP, got ${msg.type}`, 'PROTOCOL_VIOLATION');\n }\n return;\n }\n if (this.state === SessionState.IDLE) {\n // First message must be CLIENT_SETUP (client sends) or we receive it (server)\n if (this.role === EndpointRole.CLIENT) {\n throw new SetupError(\n 'Client must send CLIENT_SETUP before receiving any messages',\n 'PROTOCOL_VIOLATION',\n );\n }\n if (msg.type !== 'CLIENT_SETUP') {\n throw new SetupError(\n `First message must be CLIENT_SETUP, got ${msg.type}`,\n 'PROTOCOL_VIOLATION',\n );\n }\n } else if (this.state === SessionState.SETUP_PENDING) {\n // Waiting for response\n if (this.role === EndpointRole.CLIENT && msg.type !== 'SERVER_SETUP') {\n throw new SetupError(\n `Expected SERVER_SETUP after CLIENT_SETUP, got ${msg.type}`,\n 'PROTOCOL_VIOLATION',\n );\n }\n if (this.role === EndpointRole.SERVER) {\n throw new SetupError(\n 'Server should not receive messages while SETUP_PENDING',\n 'PROTOCOL_VIOLATION',\n );\n }\n }\n // After ESTABLISHED, other messages are valid (handled by session)\n }\n\n // ── draft-18 unified SETUP ──────────────────────────────────────────\n\n /**\n * Create a draft-18 unified SETUP message (both roles). Setup Options carry\n * the same inputs as draft-14/16 EXCEPT MAX_REQUEST_ID, which draft-18 removes\n * (QUIC stream limits replace request credit). Advances IDLE → SETUP_PENDING\n * (the side that opens) or SETUP_PENDING → ESTABLISHED (the responder).\n */\n createSetup18(options: {\n path?: string;\n authority?: string;\n implementation?: string;\n // §10.3.1.3: vi64 (full uint64) — emitted as a vi64 Setup Option below.\n maxAuthTokenCacheSize?: bigint;\n authTokens?: Uint8Array[];\n /** draft-22 §9.1.7: our MAX_REQUEST_UPDATES; ignored before draft 22. */\n maxRequestUpdates?: bigint;\n } = {}): Setup {\n const enc = new TextEncoder();\n const setupOptions: SetupOptionMap = new Map();\n if (options.path !== undefined) {\n setupOptions.set(BigInt(SetupOption18.PATH), [enc.encode(options.path)]);\n }\n if (options.authority !== undefined) {\n setupOptions.set(BigInt(SetupOption18.AUTHORITY), [enc.encode(options.authority)]);\n }\n if (options.implementation !== undefined) {\n setupOptions.set(BigInt(SetupOption18.MOQT_IMPLEMENTATION), [enc.encode(options.implementation)]);\n }\n if (options.maxAuthTokenCacheSize !== undefined) {\n setupOptions.set(BigInt(SetupOption18.MAX_AUTH_TOKEN_CACHE_SIZE), [options.maxAuthTokenCacheSize]);\n }\n if (options.authTokens !== undefined && options.authTokens.length > 0) {\n setupOptions.set(BigInt(SetupOption18.AUTHORIZATION_TOKEN), options.authTokens);\n }\n if (options.maxRequestUpdates !== undefined && isDraft22(this.draftVersion)) {\n setupOptions.set(BigInt(SetupOption22.MAX_REQUEST_UPDATES), [options.maxRequestUpdates]);\n }\n // Deliberately NO MAX_REQUEST_ID for draft-18.\n this.state = this.state === SessionState.IDLE ? SessionState.SETUP_PENDING : SessionState.ESTABLISHED;\n return { type: 'SETUP', setupOptions };\n }\n\n /**\n * Handle a received draft-18 SETUP (either role). Interprets Setup Options\n * (unknown options ignored) and advances the handshake state.\n */\n handleSetup18(msg: Setup): SetupResult {\n const result = this.extractSetupOptions18(msg.setupOptions);\n this.setupResult = result;\n this.state = this.state === SessionState.IDLE ? SessionState.SETUP_PENDING : SessionState.ESTABLISHED;\n return result;\n }\n\n /** Interpret draft-18 Setup Options into a {@link SetupResult}. */\n private extractSetupOptions18(options: SetupOptionMap): SetupResult {\n // §10.3.1.1 / §10.3.1.2: over WebTransport, PATH and AUTHORITY MUST NOT be used\n // at all — a received one closes the session with INVALID_PATH / INVALID_AUTHORITY\n // (regardless of role). This takes precedence over the native role policy below.\n if (this.webtransport) {\n if (options.has(BigInt(SetupOption18.PATH))) {\n throw new SetupError('PATH MUST NOT be used over WebTransport (§10.3.1.2)', 'INVALID_PATH');\n }\n if (options.has(BigInt(SetupOption18.AUTHORITY))) {\n throw new SetupError('AUTHORITY MUST NOT be used over WebTransport (§10.3.1.1)', 'INVALID_AUTHORITY');\n }\n }\n // Native-QUIC role policy: PATH and AUTHORITY are client→server only. If WE are\n // the client, the SETUP we are reading came from the server, which MUST NOT\n // include them.\n if (this.role === EndpointRole.CLIENT) {\n if (options.has(BigInt(SetupOption18.PATH)) || options.has(BigInt(SetupOption18.AUTHORITY))) {\n throw new SetupError('Server SETUP must not contain PATH or AUTHORITY', 'PROTOCOL_VIOLATION');\n }\n }\n\n const dec = new TextDecoder();\n let peerImplementation: string | undefined;\n let path: string | undefined;\n let authority: string | undefined;\n // draft-18 §10.3.1.3 MAX_AUTH_TOKEN_CACHE_SIZE is a vi64 (full uint64) — store\n // the raw bigint without folding it through the QUIC-varint range.\n let peerMaxAuthTokenCacheSize: bigint | undefined;\n let authTokens: AuthorizationToken[] | undefined;\n let peerMaxRequestUpdates: bigint | undefined;\n\n for (const [type, values] of options) {\n // §10.3 / §10.3.1: a known singleton Setup Option MUST NOT be repeated\n // (AUTHORIZATION_TOKEN is the only repeatable known option; unknown options\n // may be duplicated and are ignored). Reject duplicates on receive.\n if (values.length > 1 && KNOWN_SINGLETON_SETUP_OPTIONS_18.has(Number(type))) {\n throw new SetupError(\n `Repeated Setup Option 0x${type.toString(16)} (singleton)`,\n 'PROTOCOL_VIOLATION',\n );\n }\n const first = values[0];\n switch (Number(type)) {\n case SetupOption18.MOQT_IMPLEMENTATION:\n if (first instanceof Uint8Array) peerImplementation = dec.decode(first);\n break;\n case SetupOption18.PATH:\n if (first instanceof Uint8Array) path = dec.decode(first);\n break;\n case SetupOption18.AUTHORITY:\n if (first instanceof Uint8Array) authority = dec.decode(first);\n break;\n case SetupOption18.MAX_AUTH_TOKEN_CACHE_SIZE:\n if (typeof first === 'bigint') peerMaxAuthTokenCacheSize = first;\n break;\n case SetupOption18.AUTHORIZATION_TOKEN:\n // draft-18 Token internals are vi64 (full uint64), not QUIC varint.\n authTokens = values\n .filter((v): v is Uint8Array => v instanceof Uint8Array)\n .map((v) => parseAuthorizationToken18(v));\n break;\n case SetupOption22.MAX_REQUEST_UPDATES:\n // A draft-18 peer would not send it; there it is an unknown option.\n if (!isDraft22(this.draftVersion)) break;\n if (values.length > 1) {\n throw new SetupError('Repeated Setup Option MAX_REQUEST_UPDATES (singleton)', 'PROTOCOL_VIOLATION');\n }\n if (typeof first === 'bigint') peerMaxRequestUpdates = first;\n break;\n // Unknown Setup Options are ignored (§10.3).\n }\n }\n // draft-18 has no MAX_REQUEST_ID; request flow control is QUIC stream limits.\n return {\n peerMaxRequestId: varint(0n),\n ...(peerImplementation !== undefined ? { peerImplementation } : {}),\n ...(path !== undefined ? { path } : {}),\n ...(authority !== undefined ? { authority } : {}),\n ...(peerMaxAuthTokenCacheSize !== undefined ? { peerMaxAuthTokenCacheSize } : {}),\n ...(authTokens !== undefined ? { authTokens } : {}),\n ...(peerMaxRequestUpdates !== undefined ? { peerMaxRequestUpdates } : {}),\n };\n }\n\n /**\n * Create CLIENT_SETUP message (client only).\n * @param options Setup options\n * @returns CLIENT_SETUP message to send\n * @throws {Error} If not in correct state or role\n */\n createClientSetup(options: {\n maxRequestId?: Varint;\n path?: string;\n authority?: string;\n implementation?: string;\n // draft-14/16: QUIC varint — the encoder range-guards an above-range value.\n maxAuthTokenCacheSize?: bigint;\n authTokens?: Uint8Array[];\n } = {}): ClientSetup {\n if (this.role !== EndpointRole.CLIENT) {\n throw new Error('Only client can create CLIENT_SETUP');\n }\n if (this.state !== SessionState.IDLE) {\n throw new Error(`Cannot create CLIENT_SETUP in state ${this.state}`);\n }\n\n const parameters: Parameters = new Map();\n\n if (options.maxRequestId !== undefined) {\n parameters.set(varint(SetupParam.MAX_REQUEST_ID), [options.maxRequestId]);\n }\n\n if (options.path !== undefined) {\n parameters.set(varint(SetupParam.PATH), [new TextEncoder().encode(options.path)]);\n }\n\n if (options.authority !== undefined) {\n parameters.set(varint(SetupParam.AUTHORITY), [new TextEncoder().encode(options.authority)]);\n }\n\n if (options.implementation !== undefined) {\n parameters.set(\n varint(SetupParam.MOQT_IMPLEMENTATION),\n [new TextEncoder().encode(options.implementation)],\n );\n }\n\n if (options.maxAuthTokenCacheSize !== undefined) {\n parameters.set(varint(SetupParam.MAX_AUTH_TOKEN_CACHE_SIZE), [options.maxAuthTokenCacheSize]);\n }\n\n if (options.authTokens !== undefined && options.authTokens.length > 0) {\n parameters.set(varint(SetupParam.AUTHORIZATION_TOKEN), options.authTokens);\n }\n\n this.state = SessionState.SETUP_PENDING;\n\n return {\n type: 'CLIENT_SETUP',\n parameters,\n };\n }\n\n /**\n * Handle received CLIENT_SETUP (server only).\n * @param msg The CLIENT_SETUP message\n * @returns Extracted setup parameters\n * @throws {SetupError} If validation fails\n */\n handleClientSetup(msg: ClientSetup): SetupResult {\n if (this.role !== EndpointRole.SERVER) {\n throw new SetupError('Only server handles CLIENT_SETUP', 'PROTOCOL_VIOLATION');\n }\n if (this.state !== SessionState.IDLE) {\n throw new SetupError(\n `Cannot handle CLIENT_SETUP in state ${this.state}`,\n 'PROTOCOL_VIOLATION',\n );\n }\n\n const result = this.extractSetupParams(msg.parameters, true);\n this.setupResult = result;\n this.state = SessionState.SETUP_PENDING;\n\n return result;\n }\n\n /**\n * Create SERVER_SETUP message (server only).\n * @param options Setup options\n * @returns SERVER_SETUP message to send\n * @throws {Error} If not in correct state or role\n */\n createServerSetup(options: {\n maxRequestId?: Varint;\n implementation?: string;\n // draft-14/16: QUIC varint — the encoder range-guards an above-range value.\n maxAuthTokenCacheSize?: bigint;\n authTokens?: Uint8Array[];\n } = {}): ServerSetup {\n if (this.role !== EndpointRole.SERVER) {\n throw new Error('Only server can create SERVER_SETUP');\n }\n if (this.state !== SessionState.SETUP_PENDING) {\n throw new Error(`Cannot create SERVER_SETUP in state ${this.state}`);\n }\n\n const parameters: Parameters = new Map();\n\n if (options.maxRequestId !== undefined) {\n parameters.set(varint(SetupParam.MAX_REQUEST_ID), [options.maxRequestId]);\n }\n\n if (options.implementation !== undefined) {\n parameters.set(\n varint(SetupParam.MOQT_IMPLEMENTATION),\n [new TextEncoder().encode(options.implementation)],\n );\n }\n\n if (options.maxAuthTokenCacheSize !== undefined) {\n parameters.set(varint(SetupParam.MAX_AUTH_TOKEN_CACHE_SIZE), [options.maxAuthTokenCacheSize]);\n }\n\n if (options.authTokens !== undefined && options.authTokens.length > 0) {\n parameters.set(varint(SetupParam.AUTHORIZATION_TOKEN), options.authTokens);\n }\n\n this.state = SessionState.ESTABLISHED;\n\n return {\n type: 'SERVER_SETUP',\n parameters,\n };\n }\n\n /**\n * Handle received SERVER_SETUP (client only).\n * @param msg The SERVER_SETUP message\n * @returns Extracted setup parameters\n * @throws {SetupError} If validation fails\n */\n handleServerSetup(msg: ServerSetup): SetupResult {\n if (this.role !== EndpointRole.CLIENT) {\n throw new SetupError('Only client handles SERVER_SETUP', 'PROTOCOL_VIOLATION');\n }\n if (this.state !== SessionState.SETUP_PENDING) {\n throw new SetupError(\n `Cannot handle SERVER_SETUP in state ${this.state}`,\n 'PROTOCOL_VIOLATION',\n );\n }\n\n // Server must not send PATH or AUTHORITY\n if (msg.parameters.has(SetupParam.PATH)) {\n throw new SetupError('Server sent PATH parameter', 'INVALID_PATH');\n }\n if (msg.parameters.has(SetupParam.AUTHORITY)) {\n throw new SetupError('Server sent AUTHORITY parameter', 'INVALID_AUTHORITY');\n }\n\n const result = this.extractSetupParams(msg.parameters, false);\n this.setupResult = result;\n this.state = SessionState.ESTABLISHED;\n\n return result;\n }\n\n /**\n * Extract known parameters from setup message.\n * Unknown parameters are ignored per §9.3.\n * AUTHORIZATION_TOKEN may appear multiple times (§9.3.1.5).\n * Other known parameters with duplicates are protocol violations.\n */\n private extractSetupParams(params: Parameters, isClientSetup: boolean): SetupResult {\n let peerMaxRequestId: Varint = varint(0); // §9.3.1.3: absent = 0 (no requests permitted)\n let peerImplementation: string | undefined;\n let path: string | undefined;\n let authority: string | undefined;\n let peerMaxAuthTokenCacheSize: Varint | undefined;\n const authTokens: AuthorizationToken[] = [];\n\n for (const [key, values] of params) {\n // Skip unknown parameters (§9.3: \"An endpoint MUST ignore unknown setup parameters\")\n // Unknown parameters are allowed to have duplicates per §9.2\n const isKnown = key === SetupParam.MAX_REQUEST_ID ||\n key === SetupParam.MOQT_IMPLEMENTATION ||\n key === SetupParam.PATH ||\n key === SetupParam.AUTHORITY ||\n key === SetupParam.AUTHORIZATION_TOKEN ||\n key === SetupParam.MAX_AUTH_TOKEN_CACHE_SIZE;\n\n if (!isKnown) {\n continue;\n }\n\n // AUTHORIZATION_TOKEN can have multiple values (§9.3.1.5)\n if (key === SetupParam.AUTHORIZATION_TOKEN) {\n for (const rawValue of values) {\n if (!(rawValue instanceof Uint8Array)) continue;\n\n // §9.2.2.1: \"If the Token structure cannot be decoded, the receiver\n // MUST close the Session with KEY_VALUE_FORMATTING_ERROR.\"\n let token: AuthorizationToken;\n try {\n token = parseAuthorizationToken(rawValue);\n } catch {\n throw new SetupError(\n 'Malformed AUTHORIZATION_TOKEN structure',\n 'KEY_VALUE_FORMATTING_ERROR',\n );\n }\n\n // §9.2.2.1: \"If a server receives Alias Type DELETE (0x0) or\n // USE_ALIAS (0x2) in a CLIENT_SETUP message, it MUST close the\n // session with a PROTOCOL_VIOLATION.\"\n if (isClientSetup) {\n if (token.aliasType === (AliasType.DELETE as bigint) ||\n token.aliasType === (AliasType.USE_ALIAS as bigint)) {\n throw new SetupError(\n `CLIENT_SETUP AUTHORIZATION_TOKEN cannot use Alias Type ${token.aliasType} ` +\n '(DELETE and USE_ALIAS are prohibited in CLIENT_SETUP)',\n 'PROTOCOL_VIOLATION',\n );\n }\n }\n\n authTokens.push(token);\n }\n continue;\n }\n\n // Other known parameters must not have duplicates\n if (values.length > 1) {\n throw new SetupError(`Duplicate setup parameter: ${key}`, 'PROTOCOL_VIOLATION');\n }\n\n const value = values[0];\n\n if (key === SetupParam.MAX_REQUEST_ID) {\n if (typeof value === 'bigint') {\n // Request IDs are QUIC-varint range; re-validate (a raw vi64-range\n // bigint above 2^62-1 is out of range for this setup parameter).\n peerMaxRequestId = varint(value);\n }\n } else if (key === SetupParam.MOQT_IMPLEMENTATION) {\n if (value instanceof Uint8Array) {\n peerImplementation = new TextDecoder().decode(value);\n }\n } else if (key === SetupParam.MAX_AUTH_TOKEN_CACHE_SIZE) {\n // §9.3.1.4: MAX_AUTH_TOKEN_CACHE_SIZE — max bytes of token aliases\n // the peer is willing to store\n if (typeof value === 'bigint') {\n peerMaxAuthTokenCacheSize = varint(value);\n }\n } else if (key === SetupParam.PATH) {\n if (isClientSetup && value instanceof Uint8Array) {\n const pathStr = new TextDecoder().decode(value);\n this.validatePath(pathStr);\n path = pathStr;\n }\n } else if (key === SetupParam.AUTHORITY) {\n if (isClientSetup && value instanceof Uint8Array) {\n const authorityStr = new TextDecoder().decode(value);\n this.validateAuthority(authorityStr);\n authority = authorityStr;\n }\n }\n }\n\n const result: SetupResult = { peerMaxRequestId };\n if (peerImplementation !== undefined) {\n (result as { peerImplementation: string }).peerImplementation = peerImplementation;\n }\n if (path !== undefined) {\n (result as { path: string }).path = path;\n }\n if (authority !== undefined) {\n (result as { authority: string }).authority = authority;\n }\n if (peerMaxAuthTokenCacheSize !== undefined) {\n (result as { peerMaxAuthTokenCacheSize: bigint }).peerMaxAuthTokenCacheSize = peerMaxAuthTokenCacheSize;\n }\n if (authTokens.length > 0) {\n (result as { authTokens: AuthorizationToken[] }).authTokens = authTokens;\n }\n return result;\n }\n\n /**\n * Validate PATH parameter per §9.3.1.2.\n * Must be a valid path-abempty per RFC3986, optionally with query.\n * The spec explicitly allows appending ?query to PATH.\n * Valid forms: \"\", \"/\", \"/path\", \"/path?query\", \"?query\" (empty path with query)\n * @throws {SetupError} If path is malformed (MALFORMED_PATH)\n */\n private validatePath(path: string): void {\n // Split path and query first\n const queryIndex = path.indexOf('?');\n const pathPart = queryIndex === -1 ? path : path.slice(0, queryIndex);\n const queryPart = queryIndex === -1 ? '' : path.slice(queryIndex + 1);\n\n // path-abempty allows empty path, so pathPart can be \"\"\n // If pathPart is non-empty, it must start with '/' (absolute-path form)\n if (pathPart.length > 0 && !pathPart.startsWith('/')) {\n throw new SetupError('PATH path component must start with / or be empty', 'MALFORMED_PATH');\n }\n\n // Validate path part\n // path-abempty = *( \"/\" segment )\n // segment = *pchar\n // pchar = unreserved / pct-encoded / sub-delims / \":\" / \"@\"\n const pathRegex = /^(\\/[A-Za-z0-9\\-._~!$&'()*+,;=:@%]*)*$/;\n if (!pathRegex.test(pathPart)) {\n throw new SetupError('PATH contains invalid characters', 'MALFORMED_PATH');\n }\n\n // Validate query part if present\n // query = *( pchar / \"/\" / \"?\" )\n if (queryPart.length > 0) {\n const queryRegex = /^[A-Za-z0-9\\-._~!$&'()*+,;=:@%\\/?]*$/;\n if (!queryRegex.test(queryPart)) {\n throw new SetupError('PATH query contains invalid characters', 'MALFORMED_PATH');\n }\n }\n\n // Check percent encoding is valid in both parts\n const fullPath = path;\n const percentRegex = /%[0-9A-Fa-f]{2}/g;\n const strippedPath = fullPath.replace(percentRegex, '');\n if (strippedPath.includes('%')) {\n throw new SetupError('PATH contains malformed percent encoding', 'MALFORMED_PATH');\n }\n }\n\n /**\n * Validate AUTHORITY parameter per §9.3.1.1.\n * Must be a valid authority per RFC3986 §3.2 (includes userinfo, host, port).\n * The spec says \"authority portion of the URI\" and only forbids malformed values.\n * This is a lenient validator that accepts full RFC3986 authority.\n * @throws {SetupError} If authority is malformed (MALFORMED_AUTHORITY)\n */\n private validateAuthority(authority: string): void {\n if (authority.length === 0) {\n throw new SetupError('AUTHORITY cannot be empty', 'MALFORMED_AUTHORITY');\n }\n\n // RFC3986 authority = [ userinfo \"@\" ] host [ \":\" port ]\n let remaining = authority;\n\n // Extract userinfo if present (user:password@)\n const atIndex = remaining.indexOf('@');\n if (atIndex !== -1) {\n const userinfo = remaining.slice(0, atIndex);\n remaining = remaining.slice(atIndex + 1);\n\n // userinfo = *( unreserved / pct-encoded / sub-delims / \":\" )\n const userinfoRegex = /^[A-Za-z0-9\\-._~!$&'()*+,;=:%]*$/;\n if (!userinfoRegex.test(userinfo)) {\n throw new SetupError('AUTHORITY userinfo contains invalid characters', 'MALFORMED_AUTHORITY');\n }\n\n // Check percent encoding in userinfo\n if (!this.hasValidPercentEncoding(userinfo)) {\n throw new SetupError('AUTHORITY userinfo has malformed percent encoding', 'MALFORMED_AUTHORITY');\n }\n }\n\n // Extract port if present (after last colon for non-IPv6, after ] for IPv6)\n let host = remaining;\n let port: string | undefined;\n\n if (remaining.startsWith('[')) {\n // IP-literal = \"[\" ( IPv6address / IPvFuture ) \"]\"\n const bracketEnd = remaining.indexOf(']');\n if (bracketEnd === -1) {\n throw new SetupError('AUTHORITY has unclosed IP-literal bracket', 'MALFORMED_AUTHORITY');\n }\n host = remaining.slice(0, bracketEnd + 1);\n if (remaining.length > bracketEnd + 1) {\n if (remaining[bracketEnd + 1] !== ':') {\n throw new SetupError('AUTHORITY has invalid characters after IP-literal', 'MALFORMED_AUTHORITY');\n }\n port = remaining.slice(bracketEnd + 2);\n }\n\n // Validate IP-literal content (IPv6 or IPvFuture)\n const ipLiteral = host.slice(1, -1);\n\n if (ipLiteral.startsWith('v')) {\n // IPvFuture = \"v\" 1*HEXDIG \".\" 1*( unreserved / sub-delims / \":\" )\n const ipvFutureRegex = /^v[0-9A-Fa-f]+\\.[A-Za-z0-9\\-._~!$&'()*+,;=:]+$/;\n if (!ipvFutureRegex.test(ipLiteral)) {\n throw new SetupError('AUTHORITY has invalid IPvFuture', 'MALFORMED_AUTHORITY');\n }\n } else {\n // IPv6address validation per RFC3986\n this.validateIPv6(ipLiteral);\n }\n } else {\n // reg-name or IPv4\n const colonIdx = remaining.lastIndexOf(':');\n if (colonIdx !== -1) {\n const potentialPort = remaining.slice(colonIdx + 1);\n // Port must be all digits (or empty)\n if (/^\\d*$/.test(potentialPort)) {\n host = remaining.slice(0, colonIdx);\n port = potentialPort;\n }\n }\n\n // §3.1.2: \"The authority portion MUST NOT contain an empty host portion\"\n if (host.length === 0) {\n throw new SetupError('AUTHORITY MUST NOT contain an empty host portion', 'MALFORMED_AUTHORITY');\n }\n\n // Validate reg-name: *( unreserved / pct-encoded / sub-delims )\n const regNameRegex = /^[A-Za-z0-9\\-._~!$&'()*+,;=%]*$/;\n if (!regNameRegex.test(host)) {\n throw new SetupError('AUTHORITY host contains invalid characters', 'MALFORMED_AUTHORITY');\n }\n\n // Check percent encoding in host\n if (!this.hasValidPercentEncoding(host)) {\n throw new SetupError('AUTHORITY host has malformed percent encoding', 'MALFORMED_AUTHORITY');\n }\n }\n\n // Validate port if present and non-empty\n if (port !== undefined && port.length > 0) {\n const portNum = parseInt(port, 10);\n if (isNaN(portNum) || portNum < 0 || portNum > 65535) {\n throw new SetupError('AUTHORITY has invalid port number', 'MALFORMED_AUTHORITY');\n }\n }\n }\n\n /**\n * Check if a string has valid percent encoding (all % followed by 2 hex digits).\n */\n private hasValidPercentEncoding(str: string): boolean {\n const percentRegex = /%[0-9A-Fa-f]{2}/g;\n const strippedStr = str.replace(percentRegex, '');\n return !strippedStr.includes('%');\n }\n\n /**\n * Validate IPv6 address per RFC3986 §3.2.2 / RFC4291.\n * Rules:\n * - At most one \"::\" (zero-compression) is allowed\n * - \"::\" must compress at least one group (non-empty count < 8)\n * - Each segment is 1-4 hex digits\n * - 8 segments total when expanded (or embedded IPv4 suffix replaces last 2)\n * - No stray single \":\" at start or end (must be part of \"::\")\n * @throws {SetupError} If IPv6 is malformed\n */\n private validateIPv6(ipv6: string): void {\n // Check for embedded IPv4 suffix (e.g., ::ffff:192.168.1.1)\n let hasIPv4Suffix = false;\n let ipv6Part = ipv6;\n\n // IPv4 suffix is after the last colon if it contains dots\n const lastColonIdx = ipv6.lastIndexOf(':');\n if (lastColonIdx !== -1 && ipv6.slice(lastColonIdx + 1).includes('.')) {\n const ipv4Part = ipv6.slice(lastColonIdx + 1);\n // Validate IPv4 format per RFC3986 dec-octet (no leading zeros)\n const ipv4Regex = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/;\n const match = ipv4Regex.exec(ipv4Part);\n if (!match) {\n throw new SetupError('AUTHORITY has invalid embedded IPv4 in IPv6', 'MALFORMED_AUTHORITY');\n }\n // Validate each octet: 0-255, no leading zeros (except \"0\" itself)\n for (let i = 1; i <= 4; i++) {\n const octetStr = match[i]!;\n // Reject leading zeros: \"01\", \"001\", \"00\", etc. (but \"0\" is fine)\n if (octetStr.length > 1 && octetStr[0] === '0') {\n throw new SetupError('AUTHORITY has invalid embedded IPv4 in IPv6 (leading zeros)', 'MALFORMED_AUTHORITY');\n }\n const octet = parseInt(octetStr, 10);\n if (octet > 255) {\n throw new SetupError('AUTHORITY has invalid embedded IPv4 in IPv6', 'MALFORMED_AUTHORITY');\n }\n }\n hasIPv4Suffix = true;\n // Handle the case where IPv4 directly follows :: (e.g., ::192.0.2.1, 2001:db8::192.0.2.1)\n // In this case, lastColonIdx points to the second colon of ::, so include it\n if (lastColonIdx > 0 && ipv6[lastColonIdx - 1] === ':') {\n ipv6Part = ipv6.slice(0, lastColonIdx + 1);\n } else {\n ipv6Part = ipv6.slice(0, lastColonIdx);\n }\n }\n\n // Three consecutive colons is always invalid\n if (ipv6Part.includes(':::')) {\n throw new SetupError('AUTHORITY IPv6 has invalid consecutive colons', 'MALFORMED_AUTHORITY');\n }\n\n // Count \"::\" occurrences - only one allowed\n const hasDoubleColon = ipv6Part.includes('::');\n const doubleColonCount = (ipv6Part.match(/::/g) || []).length;\n if (doubleColonCount > 1) {\n throw new SetupError('AUTHORITY IPv6 has multiple zero-compression (::)', 'MALFORMED_AUTHORITY');\n }\n\n // Split into segments\n const segments = ipv6Part.split(':');\n\n // Validate each segment is 1-4 hex digits (or empty for ::)\n const hexSegmentRegex = /^[0-9A-Fa-f]{1,4}$/;\n let nonEmptyCount = 0;\n const emptyIndices: number[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n if (segment === '') {\n emptyIndices.push(i);\n } else {\n if (!hexSegmentRegex.test(segment!)) {\n throw new SetupError('AUTHORITY IPv6 has invalid segment', 'MALFORMED_AUTHORITY');\n }\n nonEmptyCount++;\n }\n }\n\n // Calculate required segments (8 for pure IPv6, 6 if IPv4 suffix)\n const requiredSegments = hasIPv4Suffix ? 6 : 8;\n\n if (hasDoubleColon) {\n // With ::, empty segments must be consecutive (they represent the ::)\n // Valid: \"::1\" → ['', '', '1'] → empties at 0,1 (consecutive)\n // Valid: \"1::\" → ['1', '', ''] → empties at 1,2 (consecutive)\n // Valid: \"1::2\" → ['1', '', '2'] → empty at 1 (single is fine)\n // Invalid: \":1::2\" → ['', '1', '', '2'] → empties at 0,2 (not consecutive)\n // Invalid: \"1::2:\" → ['1', '', '2', ''] → empties at 1,3 (not consecutive)\n for (let i = 1; i < emptyIndices.length; i++) {\n if (emptyIndices[i] !== emptyIndices[i - 1]! + 1) {\n throw new SetupError(\n 'AUTHORITY IPv6 has stray colon (empty segments not consecutive)',\n 'MALFORMED_AUTHORITY',\n );\n }\n }\n\n // :: must compress at least one group\n // If we have 8 non-empty segments (or 6 with IPv4), :: compresses nothing → invalid\n if (nonEmptyCount >= requiredSegments) {\n throw new SetupError(\n 'AUTHORITY IPv6 :: must compress at least one segment',\n 'MALFORMED_AUTHORITY',\n );\n }\n } else {\n // Without ::, must have exactly the required number of segments, all non-empty\n if (segments.length !== requiredSegments) {\n throw new SetupError(\n `AUTHORITY IPv6 must have ${requiredSegments} segments, got ${segments.length}`,\n 'MALFORMED_AUTHORITY',\n );\n }\n if (nonEmptyCount !== requiredSegments) {\n // Has empty segments but no :: → stray colons\n throw new SetupError('AUTHORITY IPv6 has empty segment without ::', 'MALFORMED_AUTHORITY');\n }\n }\n }\n}\n","/**\n * MOQT session state machine.\n *\n * Coordinates all session-level state: setup handshake, request ID allocation,\n * subscriptions, fetches, namespace discovery, and track aliases.\n *\n * This is a sans-I/O implementation: it consumes control messages and produces\n * actions to be executed by the I/O layer. No network operations are performed\n * directly.\n *\n * @see draft-ietf-moq-transport-16 §9\n * @module\n */\n\nimport { varint, type Varint } from '../primitives/varint.js';\nimport { readLocation } from '../primitives/location.js';\n\n/** vi64 ceiling (2^64 − 1) — the widest Location component any draft can\n * encode; resume-provider results above it are unencodable, so they fail\n * closed (kept local: the sans-I/O session imports no wire modules). */\nconst MAX_VI64 = 18446744073709551615n;\nimport {\n encodeFillParameters, encodeSubscriptionFilter, validateSubscriptionFilter, decodeSubscriptionFilter, subscriptionWindow, type SubscriptionFilter,\n} from '../control/subscription-filter.js';\nimport { resolveJoiningFetchRange } from './joining.js';\nimport { decodeFillParameters, resolveFillWindow, type FillOptions } from '../control/fill-parameters.js';\nimport { validateTrackNamespace, validateTrackNamespacePrefix, validateFullTrackName, isReservedSessionNamespace, isReservedDotNamespace } from '../primitives/bytes.js';\nimport { SessionError as SessionErrorCode, RequestError as RequestErrorCode } from '../errors.js';\nimport type {\n ControlMessage,\n ClientSetup,\n ServerSetup,\n Setup,\n Subscribe,\n SubscribeOk,\n SubscribeNamespace,\n SubscribeTracks,\n PublishBlocked,\n RequestUpdate,\n RequestOk,\n RequestErrorMsg,\n Fetch,\n StandaloneFetch,\n JoiningFetch,\n FetchOk,\n FetchCancel,\n PublishDone,\n PublishStateNotify,\n Unsubscribe,\n Goaway,\n MaxRequestId,\n RequestsBlocked,\n Namespace,\n NamespaceDone,\n TrackStatus,\n PublishNamespace,\n PublishNamespaceDone,\n PublishNamespaceCancel,\n PublishNamespaceOk,\n PublishNamespaceError,\n UnsubscribeNamespace,\n Publish,\n PublishOk,\n PublishError,\n} from '../control/messages.js';\nimport type { DraftVersion, DecodedControlMessage } from '../control/codec.js';\nimport {\n SessionState,\n EndpointRole,\n ForwardState,\n SubscriptionState,\n type SessionStateValue,\n type EndpointRoleValue,\n type ForwardStateValue,\n type SessionOutboundAction,\n type SendControlAction,\n type CloseConnectionAction,\n type OpenNamespaceStreamAction,\n type NotifyNamespaceAction,\n} from './types.js';\nimport { SetupGate, SetupError } from './setup.js';\nimport { RequestIdAllocator, RequestIdError } from './request-id.js';\nimport { getProtocolProfile, type ProtocolProfile } from '../profile.js';\nimport type { RequestEndpoint } from './request-endpoint.js';\nimport { SubscriptionStateMachine } from './subscription.js';\nimport { FetchStateMachine } from './fetch.js';\nimport type { GroupOrder } from '../data/types.js';\nimport { NamespaceStateMachine } from './namespace.js';\nimport { TrackAliasManager } from './track-alias.js';\nimport { MessageParam } from '../control/parameters.js';\nimport type { Parameters, ParameterValue, TrackProperties } from '../control/messages.js';\nimport { AuthTokenCache, AuthCacheError } from './auth-cache.js';\nimport { AliasType, parseAuthorizationToken, parseAuthorizationToken18, type AuthorizationToken, type ResolvedToken } from '../control/auth-token.js';\nimport { isDraft22, isRequestStreamDraft } from '../versions.js';\n\n/**\n * Set of known message parameter type codes.\n * @see draft-ietf-moq-transport-16 §9.2.2\n */\nconst KNOWN_MESSAGE_PARAMS = new Set<bigint>([\n MessageParam.DELIVERY_TIMEOUT as bigint,\n MessageParam.AUTHORIZATION_TOKEN as bigint,\n MessageParam.EXPIRES as bigint,\n MessageParam.LARGEST_OBJECT as bigint,\n MessageParam.FORWARD as bigint,\n MessageParam.SUBSCRIBER_PRIORITY as bigint,\n MessageParam.SUBSCRIPTION_FILTER as bigint,\n MessageParam.GROUP_ORDER as bigint,\n MessageParam.NEW_GROUP_REQUEST as bigint,\n MessageParam.TRACK_NAMESPACE_PREFIX as bigint, // draft-18 §10.2.14\n]);\n\n/**\n * Mapping of message parameter types to the message types where they are valid.\n * Per §9.2.2: \"If it appears in some other type of message, it MUST be ignored.\"\n * @see draft-ietf-moq-transport-16 §9.2.2\n */\nconst VALID_PARAMS_FOR_MESSAGE_TYPE: Map<bigint, Set<string>> = new Map([\n // §9.2.2.2: DELIVERY_TIMEOUT MAY appear in PUBLISH_OK, SUBSCRIBE, REQUEST_UPDATE\n [MessageParam.DELIVERY_TIMEOUT as bigint, new Set(['PUBLISH_OK', 'SUBSCRIBE', 'REQUEST_UPDATE'])],\n // §9.2.2.1: AUTHORIZATION_TOKEN MAY appear in PUBLISH, SUBSCRIBE, REQUEST_UPDATE,\n // SUBSCRIBE_NAMESPACE, PUBLISH_NAMESPACE, TRACK_STATUS, FETCH\n [MessageParam.AUTHORIZATION_TOKEN as bigint, new Set([\n 'PUBLISH', 'SUBSCRIBE', 'REQUEST_UPDATE', 'SUBSCRIBE_NAMESPACE',\n 'PUBLISH_NAMESPACE', 'TRACK_STATUS', 'FETCH',\n ])],\n // §9.2.2.6: EXPIRES MAY appear in SUBSCRIBE_OK, PUBLISH, PUBLISH_OK\n [MessageParam.EXPIRES as bigint, new Set(['SUBSCRIBE_OK', 'PUBLISH', 'PUBLISH_OK'])],\n // §9.2.2.7: LARGEST_OBJECT MAY appear in SUBSCRIBE_OK, PUBLISH, REQUEST_OK\n [MessageParam.LARGEST_OBJECT as bigint, new Set(['SUBSCRIBE_OK', 'PUBLISH', 'REQUEST_OK'])],\n // §9.2.2.8: FORWARD MAY appear in SUBSCRIBE, REQUEST_UPDATE, PUBLISH, PUBLISH_OK, SUBSCRIBE_NAMESPACE\n [MessageParam.FORWARD as bigint, new Set(['SUBSCRIBE', 'REQUEST_UPDATE', 'PUBLISH', 'PUBLISH_OK', 'SUBSCRIBE_NAMESPACE'])],\n // §9.2.2.3: SUBSCRIBER_PRIORITY MAY appear in SUBSCRIBE, FETCH, REQUEST_UPDATE, PUBLISH_OK\n [MessageParam.SUBSCRIBER_PRIORITY as bigint, new Set(['SUBSCRIBE', 'FETCH', 'REQUEST_UPDATE', 'PUBLISH_OK'])],\n // §9.2.2.5: SUBSCRIPTION_FILTER MAY appear in SUBSCRIBE, PUBLISH_OK, REQUEST_UPDATE\n [MessageParam.SUBSCRIPTION_FILTER as bigint, new Set(['SUBSCRIBE', 'PUBLISH_OK', 'REQUEST_UPDATE'])],\n // §9.2.2.4: GROUP_ORDER MAY appear in SUBSCRIBE, PUBLISH_OK, FETCH\n // Draft-14 also carries it inline on PUBLISH; handle that as a versioned exception.\n [MessageParam.GROUP_ORDER as bigint, new Set(['SUBSCRIBE', 'PUBLISH_OK', 'FETCH'])],\n // §9.2.2.9: NEW_GROUP_REQUEST MAY appear in PUBLISH_OK, SUBSCRIBE, REQUEST_UPDATE\n [MessageParam.NEW_GROUP_REQUEST as bigint, new Set(['PUBLISH_OK', 'SUBSCRIBE', 'REQUEST_UPDATE'])],\n // draft-18 §10.2.14: TRACK_NAMESPACE_PREFIX MAY appear in REQUEST_UPDATE (to\n // change the prefix of a SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS).\n [MessageParam.TRACK_NAMESPACE_PREFIX as bigint, new Set(['REQUEST_UPDATE'])],\n]);\n\n/**\n * draft-18 known message-parameter type codes (§15.7). Distinct from the\n * draft-14/16 set: draft-18 adds RENDEZVOUS_TIMEOUT, SUBGROUP_DELIVERY_TIMEOUT\n * and FILL_TIMEOUT, and treats every unknown type as a PROTOCOL_VIOLATION.\n * @see draft-ietf-moq-transport-18 §10.2, §15.7\n */\nconst KNOWN_MESSAGE_PARAMS_18 = new Set<bigint>([\n MessageParam.OBJECT_DELIVERY_TIMEOUT as bigint, // 0x02\n MessageParam.AUTHORIZATION_TOKEN as bigint, // 0x03\n MessageParam.RENDEZVOUS_TIMEOUT as bigint, // 0x04\n MessageParam.SUBGROUP_DELIVERY_TIMEOUT as bigint, // 0x06\n MessageParam.EXPIRES as bigint, // 0x08\n MessageParam.LARGEST_OBJECT as bigint, // 0x09\n MessageParam.FILL_TIMEOUT as bigint, // 0x0a\n MessageParam.FORWARD as bigint, // 0x10\n MessageParam.SUBSCRIBER_PRIORITY as bigint, // 0x20\n MessageParam.SUBSCRIPTION_FILTER as bigint, // 0x21\n MessageParam.GROUP_ORDER as bigint, // 0x22\n MessageParam.NEW_GROUP_REQUEST as bigint, // 0x32\n MessageParam.TRACK_NAMESPACE_PREFIX as bigint, // 0x34\n]);\n\n/**\n * draft-18 message-parameter scope table (§10.2.2–§10.2.14). Keys include the\n * request message types AND the response *contexts* a REQUEST_OK can answer\n * (PUBLISH_OK, REQUEST_UPDATE_OK, TRACK_STATUS_OK) plus the distinct response\n * messages SUBSCRIBE_OK — see {@link Session.d18RequestOkContext}. Per §10.2.1,\n * a known parameter appearing OUT of scope is a PROTOCOL_VIOLATION (draft-14/16\n * instead silently ignore it).\n * @see draft-ietf-moq-transport-18 §10.2.1\n */\nconst VALID_PARAMS_FOR_MESSAGE_TYPE_18: Map<bigint, Set<string>> = new Map([\n // §10.2.4\n [MessageParam.OBJECT_DELIVERY_TIMEOUT as bigint, new Set(['PUBLISH_OK', 'SUBSCRIBE', 'REQUEST_UPDATE'])],\n // §10.2.2\n [MessageParam.AUTHORIZATION_TOKEN as bigint, new Set([\n 'PUBLISH', 'SUBSCRIBE', 'REQUEST_UPDATE', 'SUBSCRIBE_NAMESPACE', 'SUBSCRIBE_TRACKS',\n 'PUBLISH_NAMESPACE', 'TRACK_STATUS', 'FETCH',\n ])],\n // §10.2.6\n [MessageParam.RENDEZVOUS_TIMEOUT as bigint, new Set(['SUBSCRIBE'])],\n // §10.2.3\n [MessageParam.SUBGROUP_DELIVERY_TIMEOUT as bigint, new Set(['PUBLISH_OK', 'SUBSCRIBE', 'REQUEST_UPDATE'])],\n // §10.2.10\n [MessageParam.EXPIRES as bigint, new Set(['SUBSCRIBE_OK', 'PUBLISH', 'PUBLISH_OK', 'REQUEST_UPDATE_OK'])],\n // §10.2.11\n [MessageParam.LARGEST_OBJECT as bigint, new Set(['SUBSCRIBE_OK', 'PUBLISH', 'REQUEST_UPDATE_OK', 'TRACK_STATUS_OK'])],\n // §10.2.5\n [MessageParam.FILL_TIMEOUT as bigint, new Set(['FETCH'])],\n // §10.2.12\n [MessageParam.FORWARD as bigint, new Set(['SUBSCRIBE', 'REQUEST_UPDATE', 'PUBLISH', 'PUBLISH_OK', 'SUBSCRIBE_TRACKS'])],\n // §10.2.7\n [MessageParam.SUBSCRIBER_PRIORITY as bigint, new Set(['SUBSCRIBE', 'FETCH', 'REQUEST_UPDATE', 'PUBLISH_OK'])],\n // §10.2.9\n [MessageParam.SUBSCRIPTION_FILTER as bigint, new Set(['SUBSCRIBE', 'PUBLISH_OK', 'REQUEST_UPDATE'])],\n // §10.2.8\n [MessageParam.GROUP_ORDER as bigint, new Set(['SUBSCRIBE', 'PUBLISH_OK', 'FETCH'])],\n // §10.2.13\n [MessageParam.NEW_GROUP_REQUEST as bigint, new Set(['PUBLISH_OK', 'SUBSCRIBE', 'REQUEST_UPDATE'])],\n // §10.2.14 — generic REQUEST_UPDATE scope; the namespace/tracks-target restriction\n // is enforced contextually in handleIncomingRequestUpdate.\n [MessageParam.TRACK_NAMESPACE_PREFIX as bigint, new Set(['REQUEST_UPDATE'])],\n]);\n\n// ─── draft-22 message parameters (draft-22 §9.20, §16.7) ─────────────\n\n/** draft-22 FILL_PARAMETERS (§9.20.15). */\nconst FILL_PARAMETERS = 0x23n;\n\n/** draft-22 Range Filters (§3.3.2): SUBGROUP, OBJECTID, PRIORITY, OBJECT_PROPERTY, TRACK_PROPERTY. */\nconst RANGE_FILTER_PARAMS_22 = new Set<bigint>([0x25n, 0x26n, 0x27n, 0x28n, 0x29n]);\n\n/** Every draft-22 message parameter (draft-22 §16.7). */\nconst KNOWN_MESSAGE_PARAMS_22 = new Set<bigint>([\n ...KNOWN_MESSAGE_PARAMS_18,\n FILL_PARAMETERS,\n ...RANGE_FILTER_PARAMS_22,\n 0x35n, // INCLUDE_PROPERTIES\n]);\n\n/**\n * draft-22 message-parameter scope table (§9.20). The same keys as the draft-18\n * table plus PUBLISH_STATE_NOTIFY; PUBLISH_OK carries only EXPIRES (the\n * subscription parameters moved to PUBLISH and REQUEST_UPDATE), 0x21 is the\n * LOCATION_FILTER, which FETCH and PUBLISH_STATE_NOTIFY also carry, and FETCH\n * gains GROUP_ORDER and SUBSCRIBER_PRIORITY. SUBSCRIBE_TRACKS inherits the\n * SUBSCRIBE parameter scope under 9.18.1.\n */\nconst VALID_PARAMS_FOR_MESSAGE_TYPE_22: Map<bigint, Set<string>> = new Map([\n [MessageParam.OBJECT_DELIVERY_TIMEOUT as bigint, new Set(['SUBSCRIBE', 'PUBLISH', 'REQUEST_UPDATE', 'SUBSCRIBE_TRACKS'])],\n [MessageParam.AUTHORIZATION_TOKEN as bigint, new Set([\n 'PUBLISH', 'SUBSCRIBE', 'REQUEST_UPDATE', 'SUBSCRIBE_NAMESPACE', 'SUBSCRIBE_TRACKS',\n 'PUBLISH_NAMESPACE', 'TRACK_STATUS', 'FETCH',\n ])],\n [MessageParam.RENDEZVOUS_TIMEOUT as bigint, new Set(['SUBSCRIBE', 'SUBSCRIBE_TRACKS'])],\n [MessageParam.SUBGROUP_DELIVERY_TIMEOUT as bigint, new Set(['SUBSCRIBE', 'PUBLISH', 'REQUEST_UPDATE', 'SUBSCRIBE_TRACKS'])],\n [MessageParam.EXPIRES as bigint, new Set(['SUBSCRIBE_OK', 'PUBLISH', 'PUBLISH_OK', 'REQUEST_UPDATE_OK',\n 'SUBSCRIBE_NAMESPACE_OK', 'SUBSCRIBE_TRACKS_OK', 'PUBLISH_NAMESPACE_OK'])],\n [MessageParam.LARGEST_OBJECT as bigint, new Set([\n 'SUBSCRIBE_OK', 'PUBLISH', 'REQUEST_UPDATE_OK', 'TRACK_STATUS_OK', 'PUBLISH_STATE_NOTIFY',\n ])],\n [MessageParam.FILL_TIMEOUT as bigint, new Set(['FETCH'])],\n [MessageParam.FORWARD as bigint, new Set(['SUBSCRIBE', 'REQUEST_UPDATE', 'PUBLISH', 'SUBSCRIBE_TRACKS', 'PUBLISH_STATE_NOTIFY'])],\n [MessageParam.SUBSCRIBER_PRIORITY as bigint, new Set(['SUBSCRIBE', 'FETCH', 'REQUEST_UPDATE', 'PUBLISH', 'SUBSCRIBE_TRACKS'])],\n [MessageParam.SUBSCRIPTION_FILTER as bigint, new Set([\n 'SUBSCRIBE', 'REQUEST_UPDATE', 'PUBLISH', 'FETCH', 'SUBSCRIBE_TRACKS', 'PUBLISH_STATE_NOTIFY',\n ])],\n [MessageParam.GROUP_ORDER as bigint, new Set(['SUBSCRIBE', 'FETCH', 'PUBLISH', 'SUBSCRIBE_TRACKS'])],\n [FILL_PARAMETERS, new Set(['SUBSCRIBE', 'REQUEST_UPDATE', 'SUBSCRIBE_TRACKS'])],\n [0x25n, new Set(['SUBSCRIBE', 'FETCH', 'REQUEST_UPDATE', 'SUBSCRIBE_TRACKS'])],\n [0x26n, new Set(['SUBSCRIBE', 'FETCH', 'REQUEST_UPDATE', 'SUBSCRIBE_TRACKS'])],\n [0x27n, new Set(['SUBSCRIBE', 'FETCH', 'REQUEST_UPDATE', 'SUBSCRIBE_TRACKS'])],\n [0x28n, new Set(['SUBSCRIBE', 'FETCH', 'REQUEST_UPDATE', 'SUBSCRIBE_TRACKS'])],\n [0x29n, new Set(['REQUEST_UPDATE', 'SUBSCRIBE_TRACKS'])],\n [MessageParam.NEW_GROUP_REQUEST as bigint, new Set(['SUBSCRIBE', 'REQUEST_UPDATE', 'SUBSCRIBE_TRACKS'])],\n [MessageParam.TRACK_NAMESPACE_PREFIX as bigint, new Set(['REQUEST_UPDATE'])],\n [0x35n, new Set(['SUBSCRIBE', 'FETCH', 'TRACK_STATUS', 'SUBSCRIBE_TRACKS'])],\n]);\n\n/**\n * Whether two Track Namespace Prefixes overlap: one is a (field-wise) prefix of\n * the other, so a single PUBLISH_NAMESPACE could match both. Used to reject an\n * overlapping incoming SUBSCRIBE_NAMESPACE with PREFIX_OVERLAP (§10.18).\n */\nfunction prefixesOverlap(a: Uint8Array[], b: Uint8Array[]): boolean {\n const shorter = a.length <= b.length ? a : b;\n const longer = a.length <= b.length ? b : a;\n for (let i = 0; i < shorter.length; i++) {\n const x = shorter[i]!, y = longer[i]!;\n if (x.length !== y.length) return false;\n for (let j = 0; j < x.length; j++) {\n if (x[j] !== y[j]) return false;\n }\n }\n return true; // every field of the shorter prefix matches the longer's\n}\n\n/**\n * Error thrown for session-level protocol violations.\n */\nexport class SessionError extends Error {\n constructor(\n message: string,\n public readonly code: 'PROTOCOL_VIOLATION' | 'INVALID_STATE' | 'RESOURCE_EXHAUSTED' | 'INVALID_RANGE',\n ) {\n super(message);\n this.name = 'SessionError';\n }\n}\n\n/**\n * Error thrown when an application API method is called after the\n * session has received a GOAWAY (state is DRAINING). Carries the\n * `newUri` from the GOAWAY so the application can reconnect.\n *\n * Applications should catch this specifically to drive reconnection\n * logic — distinct from generic `SessionError('INVALID_STATE')` which\n * covers other state-machine misuse (e.g. calling APIs before setup\n * completes).\n *\n * @see draft-ietf-moq-transport-16 §9.4 (GOAWAY)\n */\nexport class SessionDrainingError extends Error {\n constructor(\n message: string,\n /** The new-session URI from the GOAWAY, or empty string if none was provided. */\n public readonly newUri: string,\n ) {\n super(message);\n this.name = 'SessionDrainingError';\n }\n}\n\n/**\n * Options for initiating or completing setup.\n */\nexport interface SetupOptions {\n maxRequestId?: Varint;\n path?: string;\n authority?: string;\n implementation?: string;\n /**\n * Our MAX_AUTH_TOKEN_CACHE_SIZE to advertise to the peer. Declares how many bytes\n * of token aliases we are willing to cache. Default 0 = aliases prohibited. Raw\n * `bigint`: draft-18 §10.3.1.3 carries it as a vi64 (full uint64); draft-14/16\n * encode it as a QUIC varint, where an above-range value throws on encode.\n * @see draft-ietf-moq-transport-18 §10.3.1.3\n */\n maxAuthTokenCacheSize?: bigint;\n /**\n * Raw AUTHORIZATION_TOKEN parameter values to include in setup.\n * Each Uint8Array is a serialized Token structure (Figure 4).\n * @see draft-ietf-moq-transport-16 §9.3.1.5\n */\n authTokens?: Uint8Array[];\n /**\n * draft-22 §9.1.7: the MAX_REQUEST_UPDATES to advertise, the number of\n * unanswered REQUEST_UPDATEs per request stream we accept. Omitted (or 0)\n * means no limit; ignored before draft 22.\n */\n maxRequestUpdates?: bigint;\n}\n\n/** Explicit request credentials for sans-I/O callers. */\nexport interface AuthorizationOptions {\n /** Serialized MoQ Token structures, not bare CAT bytes. May repeat. */\n authTokens?: readonly Uint8Array[];\n}\n\nfunction authorizationParameters(options: AuthorizationOptions, base?: Parameters): Parameters {\n const parameters: Parameters = new Map(base);\n if (options.authTokens !== undefined) {\n if (parameters.has(MessageParam.AUTHORIZATION_TOKEN)) {\n throw new SessionError('Specify authTokens or AUTHORIZATION_TOKEN parameters, not both', 'INVALID_STATE');\n }\n if (options.authTokens.length > 0) {\n parameters.set(MessageParam.AUTHORIZATION_TOKEN, options.authTokens.map(token => Uint8Array.from(token)));\n }\n }\n return parameters;\n}\n\n/** Options for creating a subscription (draft-16 section 9.2.2). */\nexport interface SubscribeOptions extends AuthorizationOptions {\n /** §9.2.2.2: Duration in milliseconds. MUST be > 0. */\n deliveryTimeout?: Varint;\n /** §9.2.2.3: Priority relative to other subscriptions. Range 0-255. Lower = higher priority. */\n subscriberPriority?: Varint;\n /** §9.2.2.4: Ascending (0x1) or Descending (0x2). */\n groupOrder?: Varint;\n /**\n * §9.2.2.8: FORWARD parameter on the SUBSCRIBE itself (0 = paused).\n * If omitted, the subscription starts with Forward State 1 (active).\n */\n forward?: ForwardStateValue;\n /**\n * §9.2.2.5: Subscription filter.\n * If omitted, the subscription is unfiltered (all objects pass).\n */\n subscriptionFilter?: SubscriptionFilter;\n /**\n * draft-22 §3.4 only: FILL_PARAMETERS. The publisher delivers the fill range on\n * a fill fetch stream whose FETCH_HEADER carries this SUBSCRIBE's Request ID;\n * the stream ends with a FIN when complete, or is reset when the fill fails.\n * `filter` is the fill's Location filter (e.g. `RelativeStart` 1 for the\n * current group); without it the fill inherits the subscription filter. This replaces\n * the Joining FETCH, which draft 22 removed.\n */\n fill?: FillOptions;\n}\n\n/**\n * Options for sending a REQUEST_UPDATE.\n * @see draft-ietf-moq-transport-16 §9.11\n */\nexport interface RequestUpdateOptions extends AuthorizationOptions {\n forward?: ForwardStateValue;\n /** §9.2.2.3: SUBSCRIBER_PRIORITY MAY appear in REQUEST_UPDATE. */\n subscriberPriority?: Varint;\n /** §9.2.2.5: SUBSCRIPTION_FILTER MAY appear in REQUEST_UPDATE to change subscription range. */\n subscriptionFilter?: SubscriptionFilter;\n /**\n * §8 / §10.2.4: OBJECT_DELIVERY_TIMEOUT (0x02), in ms — MAY appear in REQUEST_UPDATE.\n * Changing it re-derives the effective delivery timeout the terminal-alias guard\n * must outlive; omitted leaves the current value unchanged.\n */\n objectDeliveryTimeout?: Varint;\n /** §8 / §10.2.3: SUBGROUP_DELIVERY_TIMEOUT (0x06), in ms — MAY appear in REQUEST_UPDATE (draft-18 only). */\n subgroupDeliveryTimeout?: Varint;\n /**\n * draft-18 §10.9.2 / §10.2.14: a new Track Namespace Prefix. Valid ONLY when\n * `existingRequestId` is an outbound SUBSCRIBE_NAMESPACE or SUBSCRIBE_TRACKS;\n * sets the TRACK_NAMESPACE_PREFIX (0x34) parameter and, on REQUEST_OK, replaces\n * the stored prefix. Supplying it for a normal subscription update throws (it is\n * a draft-18-only concept; there is no such update in draft-14/16).\n */\n trackNamespacePrefix?: Uint8Array[];\n /**\n * draft-22 §3.4: FILL_PARAMETERS. The publisher opens another fill fetch stream\n * whose FETCH_HEADER carries this REQUEST_UPDATE's Request ID. Draft 22 only.\n */\n fill?: FillOptions;\n}\n\n/**\n * Options for creating a fetch.\n */\nexport interface FetchOptions extends AuthorizationOptions {\n // Fetch Locations are vi64 (full uint64) on draft-18, so `bigint`. The\n // draft-14/16 FETCH encoder still range-checks them against the QUIC-varint\n // range on encode (a value above 2^62-1 throws there).\n startGroup: bigint;\n startObject: bigint;\n endGroup?: bigint;\n endObject?: bigint;\n /**\n * Requested Group Order (§9.2.2.4 / §10.2). Sets the GROUP_ORDER (0x22)\n * parameter (Ascending = 0x1, Descending = 0x2). When omitted, the parameter\n * is not sent and the response is decoded as Ascending per spec.\n */\n groupOrder?: GroupOrder;\n}\n\n/**\n * Options for creating a Joining Fetch (§9.16.2 / draft-18 §10.12.2).\n *\n * A Joining Fetch carries no namespace, name, or range — the publisher derives\n * all three from the referenced subscription, computing a range contiguous\n * with (and never overlapping) the subscription's delivery.\n */\nexport interface JoiningFetchOptions extends AuthorizationOptions {\n /** 'relative' = Fetch Type 0x2, 'absolute' = 0x3. */\n joiningFetchType: 'relative' | 'absolute';\n /** Request ID of OUR outbound SUBSCRIBE to join (NOT a track alias). */\n joiningRequestId: bigint;\n /**\n * Relative: how many groups before the largest group to start from\n * (0 = head of the current group). Absolute: the start group itself.\n * vi64 (full uint64) on draft-18; QUIC-varint-capped on 14/16 (the encoder\n * throws above that range).\n */\n joiningStart: bigint;\n /** Requested Group Order — GROUP_ORDER (0x22) parameter, as for {@link FetchOptions}. */\n groupOrder?: GroupOrder;\n}\n\n/**\n * Options for accepting an incoming FETCH (FETCH_OK metadata, §10.13). Locations\n * are vi64 (full uint64) on draft-18, so `bigint`.\n */\nexport interface FetchAcceptOptions {\n /** 1 if the End Location is the final Object in the Track, else 0. */\n endOfTrack?: number;\n /** End of the range covered by the FETCH response. */\n endLocation?: { group: bigint; object: bigint };\n /** Response parameters. */\n parameters?: Parameters;\n /** draft-18 Track Properties (§2.5) on the FETCH_OK; non-empty on draft-14/16 throws. */\n trackProperties?: TrackProperties;\n}\n\n/**\n * Options for accepting an incoming TRACK_STATUS (TRACK_STATUS_OK, §10.14).\n * `parameters` are the REQUEST_OK message parameters (as for SUBSCRIBE_OK).\n * `trackProperties` are the draft-18 Track Properties (§2.5) — draft-14/16 have\n * no such field, so supplying non-empty Track Properties there throws.\n */\nexport interface TrackStatusAcceptOptions {\n parameters?: Parameters;\n trackProperties?: TrackProperties;\n}\n\n/** Local state for an outgoing SUBSCRIBE_TRACKS request (draft-18 §10.19). */\nexport interface TrackSubscriptionState {\n /** Mutable: a §10.9.2 prefix update replaces it on REQUEST_OK. */\n trackNamespacePrefix: Uint8Array[];\n state: 'pending' | 'active' | 'terminated';\n /** draft-22 Forward State for future matching subscriptions. */\n forward: ForwardStateValue;\n /** Tracks the publisher reported it cannot serve, via PUBLISH_BLOCKED. */\n readonly blockedTracks: Array<{ trackNamespaceSuffix: Uint8Array[]; trackName: Uint8Array }>;\n}\n\n/**\n * Phase machine for a locally-cancelled request's crossed peer messages. `kind`\n * fixes which messages are legal; `phase` tracks §5.1/§5.2's \"exactly one OK-or-\n * error response\" plus the terminal:\n *\n * PENDING (no OK yet): SUBSCRIBE_OK/FETCH_OK → ESTABLISHED (kept); REQUEST_ERROR\n * → terminal (reclaim); PUBLISH_DONE → terminal (reclaim, subscribe only).\n * ESTABLISHED (OK already seen — whether before OR at cancellation, or via a\n * crossed OK): PUBLISH_DONE → terminal (reclaim, subscribe only). A SECOND\n * OK, or a REQUEST_ERROR after the OK, violates §5.1/§5.2 → §9.1 close.\n *\n * A subscription cancelled while already ESTABLISHED starts in `established`, so a\n * crossed (duplicate) SUBSCRIBE_OK for it is correctly rejected. See {@link\n * Session}'s `recentlyCancelled`.\n */\ninterface CrossedShadow {\n readonly kind: 'subscribe' | 'fetch';\n phase: 'pending' | 'established';\n}\n\n/**\n * Result of subscribe/fetch operations.\n */\nexport interface RequestResult {\n requestId: bigint;\n actions: SessionOutboundAction[];\n}\n\n/**\n * MOQT Session state machine.\n *\n * Manages the full lifecycle of a MOQT session from setup to close.\n */\nexport class Session {\n private readonly setupGate: SetupGate;\n /**\n * Per-draft behavior bundle. Today only its {@link ProtocolProfile.requestPolicy}\n * is consumed; the {@link ProtocolProfile.capabilities} flags are installed for\n * the upcoming branch-cleanup slice (they let the session key off named\n * capabilities instead of raw `_draftVersion` comparisons).\n */\n private readonly _profile: ProtocolProfile;\n private readonly requestIdAllocator: RequestIdAllocator;\n private readonly trackAliases = new TrackAliasManager();\n\n /** Outgoing subscriptions (as subscriber). */\n private readonly subscriptions = new Map<bigint, SubscriptionStateMachine>();\n /** Track info for subscriptions (for alias registration). */\n private readonly subscriptionTracks = new Map<bigint, { namespace: Uint8Array[]; name: Uint8Array }>();\n\n /**\n * PUBLISH-initiated (incoming) request ID → LOCAL outbound SUBSCRIBE request ID\n * whose cancellation is STAGED (§5.1). A pending local SUBSCRIBE colliding with\n * an inbound PUBLISH for the same track is NOT cancelled at receipt: the draft\n * requires termination only before PUBLISH_OK, and only if the PUBLISH is\n * actually accepted. Cancellation runs in {@link acceptSubscribe}; a rejected /\n * malformed / torn-down PUBLISH clears the entry and leaves the SUBSCRIBE alive.\n */\n private readonly pendingCollisions = new Map<bigint, bigint>();\n\n /**\n * SHADOWS of locally-cancelled requests (UNSUBSCRIBE / §5.1 supersession /\n * FETCH_CANCEL), keyed by request ID → a small PHASE machine for the crossed\n * peer messages that may still be in flight. §9.1 would close on any control\n * message for an unknown request; a peer response the cancellation crossed is the\n * one legitimate exception, so each shadow tracks how far through the valid crossed\n * SEQUENCE it is:\n *\n * subscribe: [ SUBSCRIBE_OK? ] → [ PUBLISH_DONE | REQUEST_ERROR ] (terminal)\n * fetch: one-shot — the FIRST crossed FETCH_OK OR REQUEST_ERROR reclaims it\n * (§5.2: after FETCH_OK the terminal is the data-stream FIN, not a\n * control message, so no later control response is legal)\n *\n * A subscribe's non-terminal SUBSCRIBE_OK is tolerated and KEEPS the shadow (a\n * PUBLISH_DONE may still follow); the phase guards §5.1/§5.2 (see\n * {@link CrossedShadow}). A wrong kind, a duplicate OK, an error-after-OK, a second\n * terminal, or a request WE never cancelled → §9.1 close.\n *\n * RECLAMATION is deterministic and TIMER-FREE. A shadow is dropped when (a) its\n * terminal arrives, or (b) draft-18 tears the request stream down\n * (`handleOutboundRequestClosed`) — draft-18 responses ride the request's OWN bidi\n * stream, so draft-18 never accumulates.\n *\n * The record is LOSSLESS and NEVER count-evicted: draft-14/16 responses share ONE\n * control stream with NO cross-request ordering guarantee (the peer MAY process\n * requests asynchronously, and reverse-direction ordering is independent of request\n * processing), so a crossing can be arbitrarily delayed — silently forgetting a\n * shadow would turn a legal crossing into a spurious §9.1 close, and there is no\n * safe signal (ordering / wall-clock timer) to reclaim on. There is also NO lossy\n * fallback (a scalar frontier cannot prove a retired id was CANCELLED vs normally\n * COMPLETED, so it wrongly tolerated duplicate responses). Absence of a shadow\n * therefore ⇒ §9.1 close. The only cost is memory for draft-14/16 cancellations\n * whose crossing never arrives (a small `{kind, phase}` record each); draft-18 —\n * the current draft — reclaims every shadow on its request-stream close.\n */\n private readonly recentlyCancelled = new Map<bigint, CrossedShadow>();\n\n /** Outgoing fetches (as fetcher). */\n private readonly fetches = new Map<bigint, FetchStateMachine>();\n\n /**\n * Outgoing PUBLISH requests (as publisher, draft-18 §10.10). We initiate a\n * PUBLISH on its own bidi request stream, advertise a Track Alias, and the peer\n * replies REQUEST_OK (PUBLISH_OK shorthand) / REQUEST_ERROR. Distinct from\n * {@link incomingSubscriptions} (peer-initiated SUBSCRIBE we serve) so response\n * correlation never crosses the two roles.\n */\n private readonly outgoingPublishes = new Map<bigint, SubscriptionStateMachine>();\n\n /** Incoming subscriptions (as publisher). */\n private readonly incomingSubscriptions = new Map<bigint, SubscriptionStateMachine>();\n\n /** Incoming fetches (as publisher). */\n private readonly incomingFetches = new Map<bigint, FetchStateMachine>();\n\n /** Outgoing namespace subscriptions (as namespace subscriber). */\n private readonly namespaceSubscriptions = new Map<bigint, NamespaceStateMachine>();\n\n /**\n * Outgoing SUBSCRIBE_TRACKS requests (draft-18 §10.19), keyed by Request ID.\n * `pending` → `active` on REQUEST_OK, `terminated` on REQUEST_ERROR / stream\n * close. `blockedTracks` accumulates PUBLISH_BLOCKED received on the response\n * stream. (PUBLISH for matched tracks arrives on its own inbound bidi stream,\n * handled by the inbound-request path.)\n */\n private readonly trackSubscriptions = new Map<bigint, TrackSubscriptionState>();\n\n /**\n * Pending REQUEST_UPDATEs: maps update requestId → pending update info.\n * `forward` applies to a subscription update; `namespacePrefix` + `prefixTarget`\n * apply a draft-18 §10.9.2 prefix change to an outbound SUBSCRIBE_NAMESPACE\n * (`'namespace'`) or SUBSCRIBE_TRACKS (`'tracks'`) on REQUEST_OK.\n */\n private readonly pendingUpdates = new Map<bigint, {\n existingRequestId: bigint;\n forward?: ForwardStateValue;\n namespacePrefix?: Uint8Array[];\n prefixTarget?: 'namespace' | 'tracks';\n // §8: STAGED delivery timeouts (ms). Applied to the subscription on REQUEST_OK,\n // NOT at send time — a REQUEST_ERROR / rollback must not commit them. `undefined`\n // means the update did not carry that parameter (leave the current value).\n objectDeliveryTimeoutMs?: number;\n subgroupDeliveryTimeoutMs?: number;\n subscriptionFilter?: Uint8Array;\n }>();\n\n /**\n * Pending outgoing TRACK_STATUS requests (as subscriber).\n * Maps requestId → track info. No subscription state created.\n * @see draft-ietf-moq-transport-16 §9.19\n */\n private readonly pendingTrackStatuses = new Map<bigint, { namespace: Uint8Array[]; name: Uint8Array }>();\n\n /**\n * PUBLISH_NAMESPACE lifecycle tracking.\n * 'pending': awaiting REQUEST_OK/PUBLISH_NAMESPACE_OK\n * 'active': accepted, can send PUBLISH_NAMESPACE_DONE\n * @see draft-ietf-moq-transport-16 §9.20, §9.22\n */\n private readonly publishedNamespaces = new Map<bigint, {\n namespace: Uint8Array[];\n state: 'pending' | 'active';\n }>();\n\n /**\n * Incoming TRACK_STATUS requests (as publisher).\n * Maps requestId → track info. No subscription state created.\n * @see draft-ietf-moq-transport-16 §9.19\n */\n private readonly incomingTrackStatuses = new Map<bigint, { namespace: Uint8Array[]; name: Uint8Array }>();\n\n /**\n * Incoming PUBLISH_NAMESPACE requests we accepted (draft-18, as the receiver).\n * Maps requestId → announced namespace. Each rides its own bidi request stream;\n * a FIN/reset of that stream withdraws the namespace (§3.3.2), handled via\n * {@link handleInboundPublishNamespaceClosed}. Populated only for draft-18,\n * where PUBLISH_NAMESPACE is a request stream rather than a control message.\n */\n private readonly incomingPublishNamespaces = new Map<bigint, { namespace: Uint8Array[] }>();\n\n /**\n * Incoming SUBSCRIBE_NAMESPACE requests we are serving as the publisher\n * (draft-18, §10.18). Each rides its own continuing bidi request stream; we\n * answer NAMESPACE / NAMESPACE_DONE on it until the subscriber cancels\n * (FIN/reset), handled via {@link handleInboundSubscribeNamespaceClosed}.\n * Kept separate from {@link namespaceSubscriptions} (our OUTBOUND, subscriber\n * side) so prefix-match scans never confuse the two roles.\n */\n private readonly incomingNamespaceSubscriptions = new Map<bigint, NamespaceStateMachine>();\n\n /**\n * Incoming SUBSCRIBE_TRACKS requests we are serving as the publisher\n * (draft-18 §10.19). Each rides its own continuing bidi request stream on which\n * we may send PUBLISH (new streams) and PUBLISH_BLOCKED until the subscriber\n * cancels (FIN/reset). Kept separate from {@link trackSubscriptions} (our\n * OUTBOUND, subscriber side) AND from {@link incomingNamespaceSubscriptions}\n * (SUBSCRIBE_NAMESPACE) so prefix-overlap checks never cross request types.\n */\n private readonly incomingTrackSubscriptions = new Map<bigint, {\n trackNamespacePrefix: Uint8Array[];\n state: 'pending' | 'active';\n forward: ForwardStateValue;\n parameters: Parameters;\n }>();\n\n /**\n * Auth token alias cache for tokens the peer registers with us.\n * Created after setup when we know our MAX_AUTH_TOKEN_CACHE_SIZE.\n * @see draft-ietf-moq-transport-16 §9.3.1.4\n */\n private authCache: AuthTokenCache | null = null;\n\n /**\n * Our own MAX_AUTH_TOKEN_CACHE_SIZE (set during setup).\n * Used to create the authCache.\n */\n private _ownMaxAuthTokenCacheSize: number = 0;\n\n /**\n * The peer's MAX_AUTH_TOKEN_CACHE_SIZE (from their setup message).\n * Limits how many token aliases we can register with them.\n * @see draft-ietf-moq-transport-16 §9.3.1.4\n */\n // draft-18 §10.3.1.3: vi64 (full uint64), so a raw `bigint`.\n private _peerMaxAuthTokenCacheSize: bigint = 0n;\n\n private _state: SessionStateValue = SessionState.IDLE;\n private _newSessionUri: string | undefined;\n private _peerMaxRequestId: Varint = varint(0n);\n private _ownMaxRequestUpdates = 0n;\n private _peerMaxRequestUpdates = 0n;\n /** draft-22 fills still expected: fill Request ID → subscription Request ID. */\n private readonly fillRequests = new Map<bigint, {\n subscriptionId: bigint;\n subscription: SubscriptionStateMachine;\n filter: SubscriptionFilter | undefined;\n groupOrder?: GroupOrder;\n ready: boolean;\n }>();\n private _goawayReceived: boolean = false;\n\n constructor(\n private readonly _role: EndpointRoleValue,\n private readonly _draftVersion: DraftVersion = 16,\n /**\n * Session-level policy. `webtransport: true` enables the §10.3.1.1/§10.3.1.2\n * rule that PATH/AUTHORITY MUST NOT appear in SETUP over WebTransport (the WT\n * adapter sets this); default `false` = native QUIC.\n */\n options: { webtransport?: boolean } = {},\n ) {\n this.setupGate = new SetupGate(_role, _draftVersion, options.webtransport ?? false);\n this._profile = getProtocolProfile(_draftVersion);\n this.requestIdAllocator = new RequestIdAllocator(_role, undefined, this._profile.requestPolicy);\n }\n\n // ─── Getters ──────────────────────────────────────────────────────────\n\n /** Current session state. */\n get state(): SessionStateValue {\n return this._state;\n }\n\n /** Endpoint role (CLIENT or SERVER). */\n get role(): EndpointRoleValue {\n return this._role;\n }\n\n /** New session URI from GOAWAY (for migration). */\n get newSessionUri(): string | undefined {\n return this._newSessionUri;\n }\n\n /** Draft version for this session. */\n get draftVersion(): DraftVersion {\n return this._draftVersion;\n }\n\n /** Peer's MAX_REQUEST_ID from setup. */\n get peerMaxRequestId(): Varint {\n return this._peerMaxRequestId;\n }\n\n /**\n * Peer's MAX_AUTH_TOKEN_CACHE_SIZE from setup.\n * Limits how many token alias bytes we can register with them.\n * @see draft-ietf-moq-transport-16 §9.3.1.4\n */\n get peerMaxAuthTokenCacheSize(): bigint {\n return this._peerMaxAuthTokenCacheSize;\n }\n\n /** draft-22 §9.1.7: the MAX_REQUEST_UPDATES we advertised (0 = no limit). */\n get ownMaxRequestUpdates(): bigint {\n return this._ownMaxRequestUpdates;\n }\n\n /** draft-22 §9.1.7: the peer's MAX_REQUEST_UPDATES (0 = no limit). */\n get peerMaxRequestUpdates(): bigint {\n return this._peerMaxRequestUpdates;\n }\n\n private registerFillRequest(requestId: bigint, sub: SubscriptionStateMachine, options: FillOptions, filter?: SubscriptionFilter): void {\n const groupOrder = options.groupOrder ?? sub.requestedGroupOrder;\n this.fillRequests.set(requestId, {\n subscriptionId: sub.requestId, subscription: sub, ready: false,\n filter: options.filter ?? filter,\n ...(groupOrder === undefined ? {} : { groupOrder }),\n });\n sub.fillRequested = true;\n }\n\n private settleFillRequest(requestId: bigint, parameters: Parameters): void {\n const fill = this.fillRequests.get(requestId);\n if (!fill) return;\n const filter = fill.filter ?? (fill.subscription.currentFilter === undefined\n ? undefined : decodeSubscriptionFilter(fill.subscription.currentFilter, 22));\n if (fill.subscription.forwardState !== ForwardState.ACTIVE\n || resolveFillWindow(filter, this.extractLargestObjectParam(parameters) ?? undefined) === null) {\n this.cancelFillRequest(requestId);\n return;\n }\n fill.ready = true;\n }\n\n /** The subscription a fill with this Request ID (a SUBSCRIBE's or a REQUEST_UPDATE's) belongs to, while one is expected. */\n fillSubscriptionFor(requestId: bigint): SubscriptionStateMachine | undefined {\n // A clean terminal response can overtake its fill data. The I/O owner\n // releases this context after consuming or explicitly cancelling the fill.\n return this.fillRequests.get(requestId)?.subscription;\n }\n\n /** Effective order for one fill, including its per-message override. */\n fillGroupOrderFor(requestId: bigint): GroupOrder | undefined {\n const fill = this.fillRequests.get(requestId);\n return fill === undefined ? undefined : fill.groupOrder ?? this.fillSubscriptionFor(requestId)?.groupOrder;\n }\n\n isFillResponsePending(requestId: bigint): boolean {\n return this.fillRequests.get(requestId)?.ready === false;\n }\n\n /** Accept the fill stream for `requestId`: it stops being expected. */\n takeFill(requestId: bigint): SubscriptionStateMachine | undefined {\n const sub = this.fillSubscriptionFor(requestId);\n this.cancelFillRequest(requestId);\n return sub;\n }\n\n /** Stop expecting the fill for `requestId`; true when one was expected. */\n cancelFillRequest(requestId: bigint): boolean {\n const fill = this.fillRequests.get(requestId);\n if (fill === undefined) return false;\n this.fillRequests.delete(requestId);\n fill.subscription.fillRequested = this.expectedFillsOf(fill.subscriptionId).length > 0;\n return true;\n }\n\n /** Request IDs of the fills still expected for subscription `subscriptionId`. */\n expectedFillsOf(subscriptionId: bigint): bigint[] {\n return [...this.fillRequests].filter(([, fill]) => fill.subscriptionId === subscriptionId).map(([id]) => id);\n }\n\n // ─── Setup Handshake ──────────────────────────────────────────────────\n\n /**\n * Initiate setup handshake (client only).\n * Creates and returns CLIENT_SETUP message.\n */\n initiateSetup(options: SetupOptions = {}): SessionOutboundAction[] {\n this.assertState(SessionState.IDLE, 'initiateSetup');\n\n if (isRequestStreamDraft(this._draftVersion)) {\n // draft-18: send the unified SETUP (no MAX_REQUEST_ID — QUIC stream limits).\n const setup = this.setupGate.createSetup18(options);\n if (isDraft22(this._draftVersion)) this._ownMaxRequestUpdates = options.maxRequestUpdates ?? 0n;\n if (options.maxAuthTokenCacheSize !== undefined) {\n this._ownMaxAuthTokenCacheSize = Number(options.maxAuthTokenCacheSize);\n }\n this._state = SessionState.SETUP_PENDING;\n return [this.sendControl(setup)];\n }\n\n const clientSetup = this.setupGate.createClientSetup(options);\n\n // Set our MAX_REQUEST_ID so validateIncoming() knows what we advertised\n if (options.maxRequestId && options.maxRequestId > 0n) {\n this.requestIdAllocator.setOurMaxRequestId(options.maxRequestId);\n }\n\n // Track our own MAX_AUTH_TOKEN_CACHE_SIZE for cache initialization\n if (options.maxAuthTokenCacheSize !== undefined) {\n this._ownMaxAuthTokenCacheSize = Number(options.maxAuthTokenCacheSize);\n }\n\n this._state = SessionState.SETUP_PENDING;\n\n return [this.sendControl(clientSetup)];\n }\n\n /**\n * Complete setup handshake (server only).\n * Creates and returns SERVER_SETUP message.\n */\n completeSetup(options: SetupOptions = {}): SessionOutboundAction[] {\n this.assertState(SessionState.SETUP_PENDING, 'completeSetup');\n\n if (this._role !== EndpointRole.SERVER) {\n throw new SessionError('Only server can call completeSetup', 'INVALID_STATE');\n }\n\n if (isRequestStreamDraft(this._draftVersion)) {\n const setup = this.setupGate.createSetup18(options);\n if (isDraft22(this._draftVersion)) this._ownMaxRequestUpdates = options.maxRequestUpdates ?? 0n;\n if (options.maxAuthTokenCacheSize !== undefined) {\n this._ownMaxAuthTokenCacheSize = Number(options.maxAuthTokenCacheSize);\n }\n this._state = SessionState.ESTABLISHED;\n return [this.sendControl(setup)];\n }\n\n const serverSetup = this.setupGate.createServerSetup(options);\n\n // Set our MAX_REQUEST_ID so validateIncoming() knows what we advertised\n if (options.maxRequestId && options.maxRequestId > 0n) {\n this.requestIdAllocator.setOurMaxRequestId(options.maxRequestId);\n }\n\n // Track our own MAX_AUTH_TOKEN_CACHE_SIZE for cache initialization\n if (options.maxAuthTokenCacheSize !== undefined) {\n this._ownMaxAuthTokenCacheSize = Number(options.maxAuthTokenCacheSize);\n }\n\n this._state = SessionState.ESTABLISHED;\n\n return [this.sendControl(serverSetup)];\n }\n\n // ─── Control Message Handling ─────────────────────────────────────────\n\n /**\n * Handle an incoming control message.\n * Returns actions to execute in response.\n */\n handleControlMessage(decoded: DecodedControlMessage, endpoint?: RequestEndpoint): SessionOutboundAction[] {\n // Draft-18 correlation seam: when this message arrived on a request stream,\n // the I/O/topology layer supplies the stream-derived Request ID (responses)\n // or update target (REQUEST_UPDATE). For draft-14/16 the Request ID is on\n // the wire and `endpoint` is omitted, so this is a passthrough that simply\n // re-types the decoded message as a fully-correlated ControlMessage.\n const msg = this.applyRequestEndpoint(decoded, endpoint);\n\n // Setup phase validation\n if (this._state === SessionState.IDLE || this._state === SessionState.SETUP_PENDING) {\n return this.handleSetupMessage(msg);\n }\n\n if (this._state === SessionState.CLOSED) {\n throw new SessionError('Session is closed', 'INVALID_STATE');\n }\n\n // §9.2: Validate message parameters for all messages that have them\n // (Setup parameters are handled separately in handleSetupMessage)\n const paramError = this.validateControlMessageParams(msg);\n if (paramError) {\n return this.closeWithError(paramError.error, paramError.reason);\n }\n\n // Dispatch based on message type.\n // §9: Invalid peer behavior (e.g., duplicate SUBSCRIBE_OK, PUBLISH_DONE\n // in wrong state) causes state machine methods to throw. Convert these\n // into close_connection actions with PROTOCOL_VIOLATION rather than\n // letting them propagate as uncaught exceptions.\n try {\n switch (msg.type) {\n case 'GOAWAY':\n return this.handleGoaway(msg);\n case 'MAX_REQUEST_ID':\n return this.handleMaxRequestId(msg);\n case 'REQUESTS_BLOCKED':\n return this.handleRequestsBlocked(msg);\n case 'SUBSCRIBE_OK':\n return this.handleSubscribeOk(msg);\n case 'REQUEST_ERROR':\n return this.handleRequestError(msg);\n case 'FETCH_OK':\n return this.handleFetchOk(msg);\n case 'REQUEST_OK':\n return this.handleRequestOk(msg);\n case 'PUBLISH_OK':\n return this.handleInboundPublishOk(msg as PublishOk);\n case 'PUBLISH_DONE':\n return this.handlePublishDone(msg);\n case 'PUBLISH_STATE_NOTIFY':\n return this.handlePublishStateNotify(msg);\n case 'UNSUBSCRIBE':\n return this.handleUnsubscribe(msg);\n case 'SUBSCRIBE':\n return this.handleIncomingSubscribe(msg);\n case 'PUBLISH':\n return this.handleIncomingPublish(msg as Publish);\n case 'FETCH':\n return this.handleIncomingFetch(msg);\n case 'FETCH_CANCEL':\n return this.handleFetchCancel(msg);\n case 'REQUEST_UPDATE':\n return this.handleIncomingRequestUpdate(msg);\n case 'TRACK_STATUS':\n return this.handleIncomingTrackStatus(msg as TrackStatus);\n case 'PUBLISH_NAMESPACE':\n return this.handleIncomingPublishNamespace(msg as PublishNamespace);\n case 'SUBSCRIBE_NAMESPACE':\n // draft-18 only: inbound SUBSCRIBE_NAMESPACE is a publisher-side\n // continuing request stream (§10.18). For draft-14/16 it is not handled\n // as an inbound message — fall through to the unsupported-type path.\n if (isRequestStreamDraft(this._draftVersion)) {\n return this.handleIncomingSubscribeNamespace(msg as SubscribeNamespace);\n }\n return this.handleUnsupportedControlMessage(msg);\n case 'SUBSCRIBE_TRACKS':\n // draft-18 only: inbound SUBSCRIBE_TRACKS is a publisher-side continuing\n // request stream (§10.19). It has no draft-14/16 inbound form.\n if (isRequestStreamDraft(this._draftVersion)) {\n return this.handleIncomingSubscribeTracks(msg as SubscribeTracks);\n }\n return this.handleUnsupportedControlMessage(msg);\n case 'PUBLISH_NAMESPACE_DONE':\n return this.handlePublishNamespaceDone(msg as PublishNamespaceDone);\n case 'PUBLISH_NAMESPACE_CANCEL':\n return this.handlePublishNamespaceCancel(msg as PublishNamespaceCancel);\n case 'PUBLISH_NAMESPACE_OK':\n return this.handlePublishNamespaceOk(msg as PublishNamespaceOk);\n case 'PUBLISH_NAMESPACE_ERROR':\n return this.handlePublishNamespaceError(msg as PublishNamespaceError);\n case 'UNSUBSCRIBE_NAMESPACE':\n return this.handleIncomingUnsubscribeNamespace(msg as UnsubscribeNamespace);\n default:\n return this.handleUnsupportedControlMessage(msg);\n }\n } catch (e) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n e instanceof Error ? e.message : String(e),\n );\n }\n }\n\n /**\n * Default handling for a control message type this session does not process.\n * Draft-14: PROTOCOL_VIOLATION (no generic REQUEST_ERROR on the wire). Draft-16+:\n * respond NOT_SUPPORTED (§3.1) when the message carries a Request ID, else ignore.\n */\n private handleUnsupportedControlMessage(msg: ControlMessage): SessionOutboundAction[] {\n if (this._draftVersion === 14) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `Unsupported message type ${msg.type}`,\n );\n }\n // §3.1: \"Limited endpoints SHOULD respond to any unsupported messages with the\n // appropriate NOT_SUPPORTED error code, rather than ignoring them.\"\n if ('requestId' in msg && typeof (msg as { requestId: unknown }).requestId === 'bigint') {\n const reqId = (msg as { requestId: bigint }).requestId;\n return [this.sendControl({\n type: 'REQUEST_ERROR',\n requestId: reqId,\n errorCode: RequestErrorCode.NOT_SUPPORTED,\n retryInterval: varint(0n),\n errorReason: `Message type ${msg.type} is not supported`,\n })];\n }\n return [];\n }\n\n /**\n * Overlay stream-derived correlation onto a decoded control message.\n *\n * Draft-14/16 carry every Request ID on the wire, so the relevant field is\n * already present and this returns the message unchanged. Draft-18 omits the\n * Request ID from responses (correlated by request stream) and omits the\n * \"Existing Request ID\" from REQUEST_UPDATE (the stream identifies the\n * target); in those cases the topology layer recovers the value from stream\n * context and supplies it here — never a placeholder.\n */\n private applyRequestEndpoint(msg: DecodedControlMessage, endpoint?: RequestEndpoint): ControlMessage {\n // No stream-derived context (draft-14/16): the wire already carried every\n // Request ID, so the decoded message is already a full ControlMessage.\n if (endpoint === undefined) {\n return msg as ControlMessage;\n }\n // REQUEST_UPDATE: the wire Request ID is the update's own (new) ID; the\n // removed \"Existing Request ID\" target comes from stream context.\n if (msg.type === 'REQUEST_UPDATE') {\n const m = msg as RequestUpdate;\n if (endpoint.existingRequestId !== undefined && m.existingRequestId === undefined) {\n // Assign the raw bigint — endpoint IDs are stream-derived and may span\n // the full draft-18 uint64 range; re-branding through varint() would\n // throw for values above the QUIC range, defeating the widening.\n return { ...m, existingRequestId: endpoint.existingRequestId } as ControlMessage;\n }\n return msg as ControlMessage;\n }\n // Responses whose Request ID is absent on the wire (draft-18) correlate via\n // the endpoint. When the wire already carried a Request ID (draft-14/16),\n // it is left untouched.\n const withId = msg as { requestId?: bigint };\n if (withId.requestId === undefined) {\n // Raw bigint — see note above; the endpoint seam carries full uint64.\n return { ...msg, requestId: endpoint.requestId } as ControlMessage;\n }\n return msg as ControlMessage;\n }\n\n private handleSetupMessage(msg: ControlMessage): SessionOutboundAction[] {\n try {\n this.setupGate.validateMessage(msg);\n\n if (msg.type === 'CLIENT_SETUP') {\n const result = this.setupGate.handleClientSetup(msg as ClientSetup);\n this._peerMaxRequestId = result.peerMaxRequestId;\n // Only update allocator if MAX_REQUEST_ID was actually provided (> 0)\n if (result.peerMaxRequestId > 0n) {\n this.requestIdAllocator.updatePeerMaxRequestId(result.peerMaxRequestId);\n }\n // §9.3.1.4: Store peer's cache size for our outbound alias registration\n if (result.peerMaxAuthTokenCacheSize !== undefined) {\n this._peerMaxAuthTokenCacheSize = result.peerMaxAuthTokenCacheSize;\n }\n // Initialize auth cache with OUR limit (server processes client tokens)\n this.initAuthCache();\n // Process client's auth tokens through our cache\n if (result.authTokens) {\n this.processSetupAuthTokens(result.authTokens, true);\n }\n this._state = SessionState.SETUP_PENDING;\n } else if (msg.type === 'SERVER_SETUP') {\n const result = this.setupGate.handleServerSetup(msg as ServerSetup);\n this._peerMaxRequestId = result.peerMaxRequestId;\n // Only update allocator if MAX_REQUEST_ID was actually provided (> 0)\n if (result.peerMaxRequestId > 0n) {\n this.requestIdAllocator.updatePeerMaxRequestId(result.peerMaxRequestId);\n }\n // §9.3.1.4: Store peer's cache size for our outbound alias registration\n if (result.peerMaxAuthTokenCacheSize !== undefined) {\n this._peerMaxAuthTokenCacheSize = result.peerMaxAuthTokenCacheSize;\n }\n // Initialize auth cache with OUR limit (client processes server tokens)\n this.initAuthCache();\n // Process server's auth tokens through our cache\n if (result.authTokens) {\n this.processSetupAuthTokens(result.authTokens, false);\n }\n this._state = SessionState.ESTABLISHED;\n } else if (msg.type === 'SETUP') {\n // draft-18 unified SETUP (role-neutral wire; this side interprets it).\n const result = this.setupGate.handleSetup18(msg as Setup);\n this._peerMaxRequestId = result.peerMaxRequestId; // 0 — draft-18 has no credit\n this._peerMaxRequestUpdates = result.peerMaxRequestUpdates ?? 0n;\n // No MAX_REQUEST_ID in draft-18: do NOT update the request-id allocator credit.\n if (result.peerMaxAuthTokenCacheSize !== undefined) {\n this._peerMaxAuthTokenCacheSize = result.peerMaxAuthTokenCacheSize;\n }\n this.initAuthCache();\n if (result.authTokens) {\n // §10.3.1.4: ANY endpoint receiving a SETUP REGISTER that exceeds its\n // MAX_AUTH_TOKEN_CACHE_SIZE MUST treat it as USE_VALUE (downgrade) rather\n // than failing with AUTH_TOKEN_CACHE_OVERFLOW — both client and server,\n // not only the server processing a CLIENT_SETUP.\n this.processSetupAuthTokens(result.authTokens, true);\n }\n this._state = this.setupGate.sessionState;\n }\n\n return [];\n } catch (e) {\n // Convert setup/request-id errors to close_connection actions\n if (e instanceof SetupError) {\n // Map SetupError codes to session error codes\n const errorCode = this.mapSetupErrorCode(e.code);\n return this.closeWithError(errorCode, e.message);\n }\n if (e instanceof RequestIdError) {\n // Map RequestIdError codes to session error codes\n const errorCode = e.code === 'PROTOCOL_VIOLATION'\n ? SessionErrorCode.PROTOCOL_VIOLATION\n : SessionErrorCode.INVALID_REQUEST_ID;\n return this.closeWithError(errorCode, e.message);\n }\n if (e instanceof AuthCacheError) {\n return this.closeWithError(e.sessionErrorCode, e.message);\n }\n // Re-throw unexpected errors\n throw e;\n }\n }\n\n /**\n * Map SetupError codes to session error codes.\n */\n private mapSetupErrorCode(code: SetupError['code']): Varint {\n switch (code) {\n case 'INVALID_PATH':\n return SessionErrorCode.INVALID_PATH;\n case 'MALFORMED_PATH':\n return SessionErrorCode.MALFORMED_PATH;\n case 'INVALID_AUTHORITY':\n return SessionErrorCode.INVALID_AUTHORITY;\n case 'MALFORMED_AUTHORITY':\n return SessionErrorCode.MALFORMED_AUTHORITY;\n case 'KEY_VALUE_FORMATTING_ERROR':\n return SessionErrorCode.KEY_VALUE_FORMATTING_ERROR;\n case 'VERSION_NEGOTIATION_FAILED':\n return SessionErrorCode.VERSION_NEGOTIATION_FAILED;\n case 'PROTOCOL_VIOLATION':\n default:\n return SessionErrorCode.PROTOCOL_VIOLATION;\n }\n }\n\n // ─── Auth Token Processing ─────────────────────────────────────────────\n\n /**\n * Initialize the auth token cache with our own MAX_AUTH_TOKEN_CACHE_SIZE.\n * @see draft-ietf-moq-transport-16 §9.3.1.4\n */\n private initAuthCache(): void {\n this.authCache = new AuthTokenCache(this._ownMaxAuthTokenCacheSize);\n }\n\n /**\n * Process AUTHORIZATION_TOKEN parameters from a setup message through the cache.\n *\n * @param tokens Parsed tokens from the setup message\n * @param isClientSetup Whether these tokens came from CLIENT_SETUP\n * @throws {AuthCacheError} for cache overflow (non-setup), duplicate aliases, unknown aliases\n * @see draft-ietf-moq-transport-16 §9.2.2.1, §9.3.1.5\n */\n private processSetupAuthTokens(tokens: AuthorizationToken[], isClientSetup: boolean): void {\n if (!this.authCache) return;\n\n for (const token of tokens) {\n this.processOneToken(token, isClientSetup);\n }\n }\n\n /**\n * Process a single auth token through the cache.\n * Returns the resolved (tokenType, tokenValue) if applicable.\n *\n * @param token Parsed token structure\n * @param isClientSetup Whether this is during CLIENT_SETUP processing\n * @returns Resolved token, or undefined for DELETE\n * @throws {AuthCacheError} for cache errors\n * @see draft-ietf-moq-transport-16 §9.2.2.1\n */\n private processOneToken(token: AuthorizationToken, isClientSetup: boolean): ResolvedToken | undefined {\n if (!this.authCache) return undefined;\n\n switch (token.aliasType) {\n case AliasType.DELETE as bigint: {\n const t = token as import('../control/auth-token.js').DeleteToken;\n this.authCache.delete(t.tokenAlias);\n return undefined;\n }\n case AliasType.REGISTER as bigint: {\n const t = token as import('../control/auth-token.js').RegisterToken;\n const result = this.authCache.register(t.tokenAlias, t.tokenType, t.tokenValue, isClientSetup);\n if (result === null) {\n // §9.3.1.5: CLIENT_SETUP REGISTER that exceeds cache → treat as USE_VALUE\n return { tokenType: t.tokenType, tokenValue: t.tokenValue };\n }\n return result;\n }\n case AliasType.USE_ALIAS as bigint: {\n const t = token as import('../control/auth-token.js').UseAliasToken;\n return this.authCache.resolve(t.tokenAlias);\n }\n case AliasType.USE_VALUE as bigint: {\n const t = token as import('../control/auth-token.js').UseValueToken;\n return { tokenType: t.tokenType, tokenValue: t.tokenValue };\n }\n default:\n return undefined;\n }\n }\n\n /**\n * Process AUTHORIZATION_TOKEN message parameters for a non-setup control message.\n *\n * Parses raw token bytes, resolves through cache, validates per-message uniqueness.\n *\n * §9.2.2.1: \"The AUTHORIZATION TOKEN parameter MAY be repeated within a message\n * as long as the combination of Token Type and Token Value are unique after\n * resolving any aliases.\"\n *\n * @returns Error info if validation fails, undefined if OK\n * @see draft-ietf-moq-transport-16 §9.2.2.1\n */\n private processMessageAuthTokens(\n values: ParameterValue[],\n ): { error: Varint; reason: string } | undefined {\n if (!this.authCache) return undefined;\n\n const resolved: ResolvedToken[] = [];\n\n // draft-18 Token internals (Alias Type / Token Alias / Token Type) are vi64\n // (full uint64); draft-14/16 use QUIC varint. Pick the wire parser by version.\n const parseToken = isRequestStreamDraft(this._draftVersion) ? parseAuthorizationToken18 : parseAuthorizationToken;\n\n for (const rawValue of values) {\n if (!(rawValue instanceof Uint8Array)) continue;\n\n // Parse token structure\n let token: AuthorizationToken;\n try {\n token = parseToken(rawValue);\n } catch {\n // §9.2.2.1: malformed → KEY_VALUE_FORMATTING_ERROR\n return {\n error: SessionErrorCode.KEY_VALUE_FORMATTING_ERROR,\n reason: 'Malformed AUTHORIZATION_TOKEN structure',\n };\n }\n\n // Process through cache\n try {\n const result = this.processOneToken(token, false);\n if (result) {\n resolved.push(result);\n }\n } catch (e) {\n if (e instanceof AuthCacheError) {\n return { error: e.sessionErrorCode, reason: e.message };\n }\n throw e;\n }\n }\n\n // §9.2.2.1: validate uniqueness of (tokenType, tokenValue) after resolution\n for (let i = 0; i < resolved.length; i++) {\n for (let j = i + 1; j < resolved.length; j++) {\n const a = resolved[i]!;\n const b = resolved[j]!;\n if (a.tokenType === b.tokenType && this.bytesEqual(a.tokenValue, b.tokenValue)) {\n return {\n error: SessionErrorCode.MALFORMED_AUTH_TOKEN,\n reason: 'Duplicate Token Type + Token Value in AUTHORIZATION_TOKEN parameters',\n };\n }\n }\n }\n\n return undefined;\n }\n\n private bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n }\n\n /**\n * Handle a GOAWAY received on the CONTROL stream (§9.4 / §10.4). This does NOT\n * close, migrate, or start timers — it transitions to DRAINING so local new\n * requests are refused, and stores the New Session URI for the application.\n * A GOAWAY arriving on a request stream is handled separately by the topology.\n */\n private handleGoaway(msg: Goaway): SessionOutboundAction[] {\n // §9.4 / §10.4: at most one GOAWAY on the control stream.\n if (this._goawayReceived) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n 'Received multiple GOAWAY messages on the control stream',\n );\n }\n\n // §9.4 / §10.4: a server MUST close if it receives a non-empty New Session URI\n // (clients cannot instruct servers to initiate connections).\n if (this._role === EndpointRole.SERVER && msg.newSessionUri.length > 0) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n 'Server received GOAWAY with non-empty New Session URI',\n );\n }\n\n // draft-18 §10.4: a control-stream GOAWAY MUST carry the Request ID, and its\n // parity MUST match the receiver's own request-id parity (the GOAWAY refers\n // to the smallest of OUR requests the peer may not have processed). Draft 22\n // (§9.2) dropped the Request ID.\n if (isRequestStreamDraft(this._draftVersion) && !isDraft22(this._draftVersion)) {\n if (msg.requestId === undefined) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n 'draft-18 control-stream GOAWAY is missing the Request ID',\n );\n }\n if ((msg.requestId & 1n) !== this.requestIdAllocator.parityBit) {\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `GOAWAY Request ID ${msg.requestId} has the wrong parity for this endpoint`,\n );\n }\n }\n\n this._goawayReceived = true;\n this._newSessionUri = msg.newSessionUri;\n this._state = SessionState.DRAINING;\n return [];\n }\n\n private handleMaxRequestId(msg: MaxRequestId): SessionOutboundAction[] {\n // §9.5: MAX_REQUEST_ID updates the peer's limit\n try {\n this.requestIdAllocator.updatePeerMaxRequestId(msg.maxRequestId);\n this._peerMaxRequestId = msg.maxRequestId;\n return [];\n } catch (e) {\n if (e instanceof RequestIdError) {\n // Non-increasing MAX_REQUEST_ID is a protocol violation\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n e.message,\n );\n }\n throw e;\n }\n }\n\n private handleRequestsBlocked(_msg: RequestsBlocked): SessionOutboundAction[] {\n // §9.6: Peer is blocked waiting to allocate request IDs.\n // Immediately replenish if we can — don't wait for the periodic check.\n // §9.5: \"An endpoint MAY send a MAX_REQUEST_ID upon receipt of REQUESTS_BLOCKED\"\n return this.maybeReplenishMaxRequestId(true);\n }\n\n /**\n * Check if the peer is approaching our MAX_REQUEST_ID limit and send\n * a new MAX_REQUEST_ID to extend their window.\n *\n * Called after every incoming request validation and on REQUESTS_BLOCKED.\n * Uses a sliding window: when the peer consumes past 50% of the current\n * window, we extend by windowSize.\n *\n * @param force If true, always replenish (used for REQUESTS_BLOCKED response)\n * @see draft-ietf-moq-transport-16 §9.5 (similar to MAX_STREAMS in RFC 9000 §4.6)\n */\n /**\n * Validate an incoming request ID and check if MAX_REQUEST_ID\n * replenishment is needed.\n *\n * Returns `{ error: actions }` on validation failure (caller should return these).\n * Returns `{ replenish: actions }` when a MAX_REQUEST_ID should be sent\n * (caller should append these to its own output).\n * Returns `{}` when validation passes with no replenishment needed.\n */\n private validateAndReplenish(requestId: bigint): {\n error?: SessionOutboundAction[];\n replenish?: SessionOutboundAction[];\n } {\n try {\n this.requestIdAllocator.validateIncoming(requestId);\n } catch (e) {\n if (e instanceof RequestIdError) {\n const errorCode = e.code === 'TOO_MANY_REQUESTS'\n ? SessionErrorCode.TOO_MANY_REQUESTS\n : SessionErrorCode.INVALID_REQUEST_ID;\n return { error: this.closeWithError(errorCode, e.message) };\n }\n throw e;\n }\n // Validation passed — check if we need to extend the peer's window\n const replenish = this.maybeReplenishMaxRequestId();\n return replenish.length > 0 ? { replenish } : {};\n }\n\n private maybeReplenishMaxRequestId(force = false): SessionOutboundAction[] {\n if (!force && !this.requestIdAllocator.shouldReplenish()) return [];\n if (this.requestIdAllocator.getOurMaxRequestId() === 0n) return [];\n\n const newMax = this.requestIdAllocator.nextReplenishValue();\n this.requestIdAllocator.commitReplenish();\n\n return [{\n type: 'send_control',\n message: {\n type: 'MAX_REQUEST_ID',\n maxRequestId: newMax,\n },\n }];\n }\n\n private handleSubscribeOk(msg: SubscribeOk): SessionOutboundAction[] {\n const sub = this.subscriptions.get(msg.requestId as bigint);\n if (!sub) {\n // §5.1: a SUBSCRIBE_OK crossing OUR cancellation of a PENDING SUBSCRIBE (we\n // unsubscribed / it was superseded) is a legal crossed message — tolerate it\n // and KEEP the shadow, because a PUBLISH_DONE terminal may still follow it on\n // the wire. A duplicate OK (shadow already ESTABLISHED — e.g. we cancelled an\n // already-established sub), a fetch-kind shadow, or a request we never\n // cancelled is a §9.1 unknown-request violation → close. (A LIVE subscription's\n // genuine duplicate SUBSCRIBE_OK is caught by the SM assert.)\n if (this.toleratesCrossedOk(msg.requestId as bigint, 'subscribe')) return [];\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${msg.requestId} for SUBSCRIBE_OK`,\n );\n }\n\n sub.handleSubscribeOk(msg.trackAlias);\n this.settleFillRequest(msg.requestId as bigint, msg.parameters);\n if (isDraft22(this._draftVersion)) {\n const properties = msg.trackProperties ?? msg.trackExtensions;\n if (properties instanceof Map) sub.publisherGroupOrder = properties.get(0x22n)?.[0] === 2n ? 'descending' : 'ascending';\n sub.locationWindow = subscriptionWindow(\n sub.currentFilter === undefined ? undefined : decodeSubscriptionFilter(sub.currentFilter, 22),\n this.extractLargestObjectParam(msg.parameters) ?? undefined,\n );\n }\n\n // Register track alias - §9.10/§10.1: duplicate alias must close connection\n const trackInfo = this.subscriptionTracks.get(msg.requestId as bigint);\n if (trackInfo) {\n try {\n this.trackAliases.register(msg.trackAlias, trackInfo.namespace, trackInfo.name, msg.requestId as bigint);\n } catch {\n return this.closeWithError(\n SessionErrorCode.DUPLICATE_TRACK_ALIAS,\n `Duplicate track alias ${msg.trackAlias}`,\n );\n }\n }\n\n return [];\n }\n\n private handleRequestError(msg: RequestErrorMsg): SessionOutboundAction[] {\n // draft-18 §10.6.2: a Redirect is only valid for certain request CONTEXTS,\n // which only the session knows (the codec parsed it blind). Validate before\n // the normal per-request dispatch terminates the request as usual.\n if (msg.redirect) {\n const invalid = this.validateRedirectContext(msg);\n if (invalid) return invalid;\n }\n\n // Could be for pending REQUEST_UPDATE\n const pending = this.pendingUpdates.get(msg.requestId as bigint);\n if (pending) {\n this.pendingUpdates.delete(msg.requestId as bigint);\n // Don't apply the update — it was rejected, and it opens no fill.\n this.fillRequests.delete(msg.requestId as bigint);\n return [];\n }\n\n // §9.19: Could be for TRACK_STATUS — no subscription state to update\n const trackStatus = this.pendingTrackStatuses.get(msg.requestId as bigint);\n if (trackStatus) {\n this.pendingTrackStatuses.delete(msg.requestId as bigint);\n return [];\n }\n\n // Could be for subscription or fetch\n const sub = this.subscriptions.get(msg.requestId as bigint);\n if (sub) {\n sub.handleRequestError(msg.errorCode, msg.errorReason);\n for (const fillId of this.expectedFillsOf(sub.requestId)) this.cancelFillRequest(fillId);\n // §5.1: REQUEST_ERROR is terminal for the (pending) subscribe — RECLAIM the\n // full state (bounded). A second REQUEST_ERROR then takes the unknown-request\n // path below.\n if (sub.trackAlias !== undefined) this.trackAliases.unregister(sub.trackAlias, sub.requestId);\n this.subscriptions.delete(msg.requestId as bigint);\n this.subscriptionTracks.delete(msg.requestId as bigint);\n return [];\n }\n\n const fetch = this.fetches.get(msg.requestId as bigint);\n if (fetch) {\n // §5.2: REQUEST_ERROR is terminal for the fetch — RECLAIM the state (bounded).\n // A second REQUEST_ERROR then takes the unknown-request / crossed-shadow path.\n fetch.handleRequestError(msg.errorCode, msg.errorReason);\n this.fetches.delete(msg.requestId as bigint);\n return [];\n }\n\n // Draft-14: SUBSCRIBE_NAMESPACE_ERROR arrives as normalized REQUEST_ERROR\n const nsSubErr = this.namespaceSubscriptions.get(msg.requestId as bigint);\n if (nsSubErr) {\n nsSubErr.handleRequestError(msg.errorCode, msg.errorReason);\n return [];\n }\n\n // draft-18 §10.19: SUBSCRIBE_TRACKS rejected (REQUEST_ERROR on its stream).\n const trackSubErr = this.trackSubscriptions.get(msg.requestId as bigint);\n if (trackSubErr) {\n trackSubErr.state = 'terminated';\n this.trackSubscriptions.delete(msg.requestId as bigint);\n return [];\n }\n\n // PUBLISH_NAMESPACE rejection (§9.20)\n const pubNsErr = this.publishedNamespaces.get(msg.requestId as bigint);\n if (pubNsErr && pubNsErr.state === 'pending') {\n this.publishedNamespaces.delete(msg.requestId as bigint);\n return [];\n }\n\n // draft-18 §10.10: our outbound PUBLISH rejected — terminate only that publish.\n const outPubErr = this.outgoingPublishes.get(msg.requestId as bigint);\n if (outPubErr) {\n this.outgoingPublishes.delete(msg.requestId as bigint);\n this.incomingJoiningLocations.delete(msg.requestId as bigint);\n return [];\n }\n\n // §5.1/§5.2: a REQUEST_ERROR crossing OUR cancellation of a still-PENDING request\n // (SUBSCRIBE or FETCH) is a legal crossed terminal — tolerate it and reclaim the\n // shadow. A REQUEST_ERROR after the request's OK already crossed (shadow\n // ESTABLISHED) violates \"exactly one OK-or-error\" → close.\n if (this.consumeCrossedTerminal(msg.requestId as bigint, 'request_error', 'subscribe', 'fetch')) return [];\n\n // §9.1: Unknown request ID must close with INVALID_REQUEST_ID.\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${msg.requestId} for REQUEST_ERROR`,\n );\n }\n\n /**\n * Validate a Redirect against the request CONTEXT it answers (§10.6.2). Returns\n * a session-close action if invalid, or `null` if the Redirect is acceptable\n * (the normal handleRequestError dispatch then terminates the request).\n *\n * - REDIRECT is valid only for SUBSCRIBE / FETCH / TRACK_STATUS /\n * PUBLISH_NAMESPACE / SUBSCRIBE_NAMESPACE responses; on any other request\n * type (REQUEST_UPDATE / SUBSCRIBE_TRACKS / PUBLISH / …) it is a violation.\n * - A server endpoint MUST NOT receive a non-empty Connect URI.\n * - A namespace-scoped request (PUBLISH_NAMESPACE / SUBSCRIBE_NAMESPACE) MUST\n * NOT carry a replacement Track Name.\n */\n private validateRedirectContext(msg: RequestErrorMsg): SessionOutboundAction[] | null {\n const rid = msg.requestId as bigint;\n const redirect = msg.redirect!;\n\n // §10.6.2: a server never receives a relocation to another session.\n if (this._role === EndpointRole.SERVER && redirect.connectUri.length > 0) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n 'REQUEST_ERROR Redirect with a non-empty Connect URI received by a server endpoint (§10.6.2)',\n );\n }\n\n // Request types for which REDIRECT is explicitly NOT allowed.\n if (this.pendingUpdates.has(rid) || this.trackSubscriptions.has(rid) || this.outgoingPublishes.has(rid)) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `REQUEST_ERROR Redirect is not valid for this request type (request ${rid})`,\n );\n }\n\n const isPubNs = this.publishedNamespaces.has(rid);\n const isSubNs = this.namespaceSubscriptions.has(rid);\n const allowed = this.subscriptions.has(rid) || this.fetches.has(rid)\n || this.pendingTrackStatuses.has(rid) || isPubNs || isSubNs;\n if (!allowed) {\n // Unknown request ID — let the normal dispatch close with INVALID_REQUEST_ID.\n return null;\n }\n\n // §10.6.2: a namespace-scoped request must not carry a replacement Track Name.\n if ((isPubNs || isSubNs) && redirect.trackName.length > 0) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n 'REQUEST_ERROR Redirect for a namespace-scoped request must have an empty Track Name (§10.6.2)',\n );\n }\n\n return null; // valid — terminate the request as usual\n }\n\n private handleFetchOk(msg: FetchOk): SessionOutboundAction[] {\n const fetch = this.fetches.get(msg.requestId as bigint);\n if (!fetch) {\n // A FETCH_OK crossing OUR FETCH_CANCEL of a PENDING fetch is a legal crossed\n // message — tolerate it and KEEP the shadow. A duplicate FETCH_OK (shadow\n // already ESTABLISHED), a subscribe-kind shadow, or a request we never\n // cancelled is a §9.1 unknown-request violation → close.\n if (this.toleratesCrossedOk(msg.requestId as bigint, 'fetch')) return [];\n // §9.1: Unknown request ID must close with INVALID_REQUEST_ID\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${msg.requestId} for FETCH_OK`,\n );\n }\n\n // §9.17: \"If End Location is smaller than the Start Location in the\n // corresponding FETCH the receiver MUST close the session with a\n // PROTOCOL_VIOLATION.\"\n if (msg.endLocation && fetch.startGroup !== undefined) {\n const endGroup = msg.endLocation.group;\n const endObject = msg.endLocation.object;\n const startGroup = fetch.startGroup;\n const startObject = fetch.startObject ?? 0n;\n\n if (\n endGroup < startGroup ||\n (endGroup === startGroup && endObject < startObject)\n ) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `FETCH_OK endLocation (${endGroup},${endObject}) < startLocation (${startGroup},${startObject})`,\n );\n }\n }\n\n // §5.2: a duplicate FETCH_OK for a live fetch is a violation → close.\n if (fetch.responseReceived) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `Duplicate FETCH_OK for request ${msg.requestId}`,\n );\n }\n fetch.handleFetchOk();\n // §10.13: FETCH_OK may arrive AFTER the data stream already ended. Reclaim only\n // once BOTH are done; if the data has NOT finished, keep the fetch (a later\n // data-stream FIN reclaims it) — see handleFetchStreamFinished.\n if (fetch.isFetcherComplete) this.fetches.delete(msg.requestId as bigint);\n return [];\n }\n\n /**\n * Terminate a pending OUTGOING subscription or fetch as a malformed track\n * (§2.4.2) — e.g. a SUBSCRIBE_OK / FETCH_OK whose Track Properties carried a\n * data-Object-only Property (wrong scope, not a wire-format error). This is NOT\n * a session close: only the one request is reset (the I/O layer RESETs its\n * stream). No-op for an unknown request ID.\n *\n * @see draft-ietf-moq-transport-18 §2.4.2\n */\n handleMalformedTrack(requestId: bigint): SessionOutboundAction[] {\n const sub = this.subscriptions.get(requestId as bigint);\n if (sub) {\n if (!sub.isTerminated) sub.handleRequestError(RequestErrorCode.MALFORMED_TRACK, 'malformed track');\n return [];\n }\n const fetch = this.fetches.get(requestId as bigint);\n if (fetch && !fetch.isCompleted) {\n fetch.handleRequestError(RequestErrorCode.MALFORMED_TRACK, 'malformed track');\n }\n return [];\n }\n\n /**\n * draft-18: resolve which response a REQUEST_OK stands in for, by the outbound\n * request state it correlates to. Returns the effective response message type\n * used for parameter-scope validation (§10.2), or `undefined` if the Request ID\n * matches no pending request (the caller then closes with INVALID_REQUEST_ID).\n * Each Request ID lives in exactly one of these maps.\n */\n private d18RequestOkContext(requestId: bigint): string | undefined {\n if (this.pendingUpdates.has(requestId)) return 'REQUEST_UPDATE_OK';\n if (this.pendingTrackStatuses.has(requestId)) return 'TRACK_STATUS_OK';\n if (this.namespaceSubscriptions.has(requestId)) return 'SUBSCRIBE_NAMESPACE_OK';\n if (this.trackSubscriptions.has(requestId)) return 'SUBSCRIBE_TRACKS_OK';\n if (this.publishedNamespaces.has(requestId)) return 'PUBLISH_NAMESPACE_OK';\n if (this.outgoingPublishes.has(requestId)) return 'PUBLISH_OK';\n return undefined;\n }\n\n private handleRequestOk(msg: RequestOk): SessionOutboundAction[] {\n // draft-18 Track Properties are CONTEXT-dependent on a REQUEST_OK: valid only\n // when it answers a TRACK_STATUS (TRACK_STATUS_OK). For every other REQUEST_OK\n // context (REQUEST_UPDATE / PUBLISH_NAMESPACE / SUBSCRIBE_NAMESPACE /\n // SUBSCRIBE_TRACKS / PUBLISH responses) they MUST be empty — the codec decoded\n // them blind, so we enforce the request-stream context here.\n if (((msg.trackProperties ?? msg.trackExtensions)?.size ?? 0) > 0 && !this.pendingTrackStatuses.has(msg.requestId as bigint)) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `Track Properties are not valid on this REQUEST_OK (request ${msg.requestId})`,\n );\n }\n\n // draft-18 §10.2.1: validate the REQUEST_OK's Message Parameters against the\n // scope of the RESPONSE it stands in for, resolved from the request stream\n // (PUBLISH_OK / REQUEST_UPDATE_OK / TRACK_STATUS_OK / namespace responses).\n if (isRequestStreamDraft(this._draftVersion) && (msg.parameters?.size ?? 0) > 0) {\n const context = this.d18RequestOkContext(msg.requestId as bigint);\n if (context) {\n const paramError = this.validateMessageParams(msg.parameters, context);\n if (paramError) return this.closeWithError(paramError.error, paramError.reason);\n }\n // context === undefined → unknown request id; the dispatch below closes with\n // INVALID_REQUEST_ID, so no parameter validation is needed here.\n }\n\n const pending = this.pendingUpdates.get(msg.requestId as bigint);\n if (pending) {\n this.pendingUpdates.delete(msg.requestId as bigint);\n\n // Apply the update to the subscription (outgoing or, for a draft-18\n // PUBLISH-initiated subscription, the incoming one).\n if (pending.subscriptionFilter !== undefined) {\n const sub = this.subscriptions.get(pending.existingRequestId) ?? this.incomingSubscriptions.get(pending.existingRequestId);\n if (sub && sub.state !== SubscriptionState.TERMINATED) {\n sub.currentFilter = pending.subscriptionFilter;\n sub.locationWindow = subscriptionWindow(\n decodeSubscriptionFilter(pending.subscriptionFilter, 22),\n this.extractLargestObjectParam(msg.parameters) ?? undefined,\n );\n }\n }\n if (pending.forward !== undefined) {\n if (pending.prefixTarget === 'tracks') {\n const tracks = this.trackSubscriptions.get(pending.existingRequestId);\n if (tracks?.state === 'active') tracks.forward = pending.forward;\n }\n const sub = this.subscriptions.get(pending.existingRequestId)\n ?? this.incomingSubscriptions.get(pending.existingRequestId);\n if (sub) {\n // d18: the update may have been sent while the subscription was\n // still PENDING (§10.12.2 races); use the pre-establish setter then.\n // A deferred ack can also arrive AFTER the subscription was\n // terminated by its initial REQUEST_ERROR (§10.8 rejection path):\n // the ack settles the update, but there is no live subscription to\n // mutate — applying would throw and close the session.\n if (sub.state === 'pending') sub.setInitialForwardState(pending.forward);\n else if (sub.state === 'established') sub.updateForwardState(pending.forward);\n // terminated: settle without applying.\n }\n }\n // Earlier FIFO responses can change the inherited filter and Forward\n // state. Decide whether this fill exists only after committing this ack.\n this.settleFillRequest(msg.requestId as bigint, msg.parameters);\n\n // §8: COMMIT the staged delivery timeouts now that the update is confirmed\n // (never at send — see requestUpdate). Omitted parameters leave the current\n // value unchanged. Applies to the outbound or the PUBLISH-initiated incoming sub.\n if (pending.objectDeliveryTimeoutMs !== undefined || pending.subgroupDeliveryTimeoutMs !== undefined) {\n const tsub = this.subscriptions.get(pending.existingRequestId)\n ?? this.incomingSubscriptions.get(pending.existingRequestId);\n if (tsub) {\n if (pending.objectDeliveryTimeoutMs !== undefined) tsub.requestedDeliveryTimeoutMs = pending.objectDeliveryTimeoutMs;\n if (pending.subgroupDeliveryTimeoutMs !== undefined) tsub.requestedSubgroupDeliveryTimeoutMs = pending.subgroupDeliveryTimeoutMs;\n }\n }\n\n // draft-18 §10.9.2: a prefix update is confirmed — replace the stored\n // Track Namespace Prefix on the outbound namespace/tracks subscription.\n if (pending.namespacePrefix && pending.prefixTarget) {\n if (pending.prefixTarget === 'namespace') {\n this.namespaceSubscriptions.get(pending.existingRequestId)?.updatePrefix(pending.namespacePrefix);\n } else {\n const ts = this.trackSubscriptions.get(pending.existingRequestId);\n if (ts) ts.trackNamespacePrefix = pending.namespacePrefix;\n }\n }\n\n return [];\n }\n\n // §9.19: TRACK_STATUS response — REQUEST_OK with track status params\n const trackStatus = this.pendingTrackStatuses.get(msg.requestId as bigint);\n if (trackStatus) {\n this.pendingTrackStatuses.delete(msg.requestId as bigint);\n return [];\n }\n\n // Draft-14: SUBSCRIBE_NAMESPACE_OK arrives as normalized REQUEST_OK\n const nsSubOk = this.namespaceSubscriptions.get(msg.requestId as bigint);\n if (nsSubOk) {\n nsSubOk.handleRequestOk();\n return [];\n }\n\n // draft-18 §10.19: SUBSCRIBE_TRACKS accepted (REQUEST_OK on its stream).\n const trackSubOk = this.trackSubscriptions.get(msg.requestId as bigint);\n if (trackSubOk) {\n trackSubOk.state = 'active';\n return [];\n }\n\n // PUBLISH_NAMESPACE response (§9.20)\n const pubNs = this.publishedNamespaces.get(msg.requestId as bigint);\n if (pubNs && pubNs.state === 'pending') {\n pubNs.state = 'active';\n return [];\n }\n\n // draft-18 §10.10: our outbound PUBLISH accepted (REQUEST_OK = PUBLISH_OK\n // shorthand). Establish the publisher-side subscription and APPLY the\n // subscriber's response parameters — the subscriber sets the Forward\n // State and filter in its acceptance (§5.1 / §9.2.2), and the joining\n // gates consult exactly this state.\n const outPub = this.outgoingPublishes.get(msg.requestId as bigint);\n if (outPub && outPub.state === SubscriptionState.PENDING) {\n outPub.acceptOutboundPublish();\n this.applyPublishAcceptanceParams(outPub, msg.parameters);\n return [];\n }\n\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${msg.requestId} for REQUEST_OK`,\n );\n }\n\n /**\n * Apply a publish acceptance's parameters to our outbound publish. FORWARD\n * defaults to 1 when omitted from PUBLISH_OK / d18 REQUEST_OK; filter and\n * delivery-timeout parameters apply only when present.\n */\n private applyPublishAcceptanceParams(sub: SubscriptionStateMachine, parameters: Parameters | undefined): void {\n // Draft 22 PUBLISH_OK carries only EXPIRES; subscription parameters remain\n // those sent in PUBLISH until an accepted REQUEST_UPDATE changes them.\n if (isDraft22(this._draftVersion)) return;\n const fwd = parameters?.get(MessageParam.FORWARD)?.[0];\n // FORWARD omitted from an acceptance defaults to 1. Only omission from a\n // REQUEST_UPDATE preserves the existing state (§9.2.2.8 / d18 §10.2.12).\n sub.updateForwardState(\n typeof fwd === 'bigint' && fwd === 0n\n ? ForwardState.PAUSED\n : ForwardState.ACTIVE,\n );\n const filter = parameters?.get(MessageParam.SUBSCRIPTION_FILTER)?.[0];\n if (filter instanceof Uint8Array) {\n try { sub.setRemoteFilterType(decodeSubscriptionFilter(filter, this._draftVersion).type); }\n catch { /* malformed bytes rejected by parameter validation */ }\n }\n const objTimeout = parameters?.get(MessageParam.OBJECT_DELIVERY_TIMEOUT)?.[0];\n if (typeof objTimeout === 'bigint') sub.requestedDeliveryTimeoutMs = Number(objTimeout);\n const subgroupTimeout = parameters?.get(MessageParam.SUBGROUP_DELIVERY_TIMEOUT)?.[0];\n if (typeof subgroupTimeout === 'bigint') sub.requestedSubgroupDeliveryTimeoutMs = Number(subgroupTimeout);\n }\n\n /** draft-14/16 §9.14: an inbound PUBLISH_OK accepting OUR outbound PUBLISH. */\n private handleInboundPublishOk(msg: PublishOk): SessionOutboundAction[] {\n const outPub = this.outgoingPublishes.get(msg.requestId as bigint);\n if (outPub && outPub.state === SubscriptionState.PENDING) {\n outPub.acceptOutboundPublish();\n this.applyPublishAcceptanceParams(outPub, msg.parameters);\n return [];\n }\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${msg.requestId} for PUBLISH_OK`,\n );\n }\n\n /**\n * draft-22 §9.10: the publisher reports, on a subscription's request stream,\n * the Largest Object it has sent, its Forward State and the Location Filter in\n * effect. The Largest Object advances the subscription's; the rest is the\n * publisher's view and is surfaced to the application as the message itself.\n * The topology accepts the message only on a SUBSCRIBE (or inbound PUBLISH)\n * stream; one that crosses our own cancellation of the request is ignored.\n */\n private handlePublishStateNotify(msg: PublishStateNotify): SessionOutboundAction[] {\n if (!isDraft22(this._draftVersion)) return this.handleUnsupportedControlMessage(msg);\n const requestId = msg.requestId;\n const incoming = requestId !== undefined ? this.incomingSubscriptions.get(requestId) : undefined;\n const sub = requestId !== undefined\n ? this.subscriptions.get(requestId) ?? (incoming?.isPublishInitiated ? incoming : undefined) : undefined;\n if (sub === undefined) return [];\n const largest = msg.parameters.get(MessageParam.LARGEST_OBJECT)?.[0];\n if (largest !== undefined && typeof largest === 'object' && 'group' in largest) {\n sub.updateLargestLocation(largest.group, largest.object);\n }\n return [];\n }\n\n private handlePublishDone(msg: PublishDone): SessionOutboundAction[] {\n // §9.19: \"The publisher does not send PUBLISH_DONE for this request\"\n if (this.pendingTrackStatuses.has(msg.requestId as bigint)) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `Received PUBLISH_DONE for TRACK_STATUS request ${msg.requestId}`,\n );\n }\n\n const sub = this.subscriptions.get(msg.requestId as bigint);\n if (!sub) {\n // §5.1.1: a PUBLISH_DONE crossing OUR cancellation of that SUBSCRIBE (from\n // EITHER phase — with or without a preceding crossed SUBSCRIBE_OK) is the legal\n // TERMINAL of the crossed sequence — tolerate it and reclaim the shadow. A\n // PUBLISH_DONE for a request we never cancelled (e.g. a second terminal after a\n // peer REQUEST_ERROR) or of a non-subscribe kind is a §9.1 violation → close.\n if (this.consumeCrossedTerminal(msg.requestId as bigint, 'publish_done', 'subscribe')) return [];\n // §9.1: Unknown request ID must close with INVALID_REQUEST_ID\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${msg.requestId} for PUBLISH_DONE`,\n );\n }\n\n // §5.1.1 / §11.1: PUBLISH_DONE is the terminal for an ESTABLISHED subscriber\n // subscription. Apply it, release the Track Alias, and RECLAIM the full state\n // (bounded — nothing retained). A SECOND PUBLISH_DONE then takes the\n // unknown-request path above (a crossed PUBLISH_DONE after OUR own unsubscribe\n // is handled by the ESTABLISHED-phase superseded record, not a retained SM).\n sub.handlePublishDone(msg.statusCode, msg.errorReason);\n if (sub.trackAlias !== undefined) this.trackAliases.unregister(sub.trackAlias, sub.requestId);\n this.subscriptions.delete(msg.requestId as bigint);\n this.subscriptionTracks.delete(msg.requestId as bigint);\n return [];\n }\n\n private handleUnsubscribe(msg: Unsubscribe): SessionOutboundAction[] {\n // Publisher-side: terminate the subscription and RECLAIM its full state\n // (bounded — nothing retained). UNSUBSCRIBE is terminal; a second one for the\n // same request then takes the unknown-request path below.\n const sub = this.incomingSubscriptions.get(msg.requestId as bigint);\n if (sub) {\n sub.handleUnsubscribe();\n if (sub.trackAlias !== undefined) this.trackAliases.unregister(sub.trackAlias, sub.requestId);\n this.incomingSubscriptions.delete(msg.requestId as bigint);\n this.incomingJoiningLocations.delete(msg.requestId as bigint);\n return [];\n }\n\n // §9.1: Unknown request ID\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${msg.requestId} for UNSUBSCRIBE`,\n );\n }\n\n /**\n * Draft-18 §2.4.1 defensive name validation for inbound full-track-name requests.\n * The codec validates on decode, but a message can be constructed directly and\n * passed to {@link handleControlMessage} (tests / programmatic APIs), so we re-check\n * BEFORE any state/alias/request map is mutated. Returns a PROTOCOL_VIOLATION close\n * action on violation, or `null` when valid (or not draft-18, which keeps legacy\n * behavior unchanged). An empty namespace is permitted (allowEmptyNamespace).\n */\n private validateFullName18(namespace: Uint8Array[], trackName: Uint8Array): SessionOutboundAction[] | null {\n if (!isRequestStreamDraft(this._draftVersion)) return null;\n try {\n validateFullTrackName(namespace, trackName, { allowEmptyNamespace: true });\n } catch (e) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n e instanceof Error ? e.message : 'invalid Full Track Name (§2.4.1)',\n );\n }\n return null;\n }\n\n /** Draft-18 §2.4.1 defensive validation for a full Track Namespace (no track name,\n * e.g. PUBLISH_NAMESPACE). Same contract as {@link validateFullName18}. */\n private validateNamespace18(namespace: Uint8Array[]): SessionOutboundAction[] | null {\n if (!isRequestStreamDraft(this._draftVersion)) return null;\n try {\n validateTrackNamespace(namespace, { allowEmpty: true });\n } catch (e) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n e instanceof Error ? e.message : 'invalid Track Namespace (§2.4.1)',\n );\n }\n return null;\n }\n\n private handleIncomingSubscribe(msg: Subscribe): SessionOutboundAction[] {\n // §2.4.1: validate the Full Track Name BEFORE creating any state.\n const nameError = this.validateFullName18(msg.trackNamespace, msg.trackName);\n if (nameError) return nameError;\n\n // Validate incoming request ID and auto-replenish MAX_REQUEST_ID §9.5\n const validated = this.validateAndReplenish(msg.requestId);\n if (validated.error) return validated.error;\n\n const filterError = this.rejectUnsupportedRangeFilters(msg.requestId, msg.parameters);\n if (filterError) return filterError;\n\n // §3.2: reserved `.`/`.session` namespace → REQUEST_ERROR DOES_NOT_EXIST.\n const reserved = this.rejectReservedNamespace(msg.trackNamespace, msg.requestId, validated.replenish);\n if (reserved) return reserved;\n\n // Create publisher-side subscription state machine\n const sub = SubscriptionStateMachine.createAsPublisher(\n msg.requestId,\n msg.trackNamespace,\n msg.trackName,\n );\n\n // §5.1: at most ONE subscription per Track per ROLE (drafts 16/18 only —\n // draft-14 defines neither this rule nor DUPLICATE_SUBSCRIPTION 0x19, and\n // draft 22 §3.1 allows concurrent subscriptions to one Track). A peer\n // SUBSCRIBE makes US the publisher; if we already hold a publisher-role\n // subscription for the same Full Track Name — an inbound SUBSCRIBE (incl. one\n // Pending) OR a LOCAL outbound PUBLISH — the REQUEST MUST fail with\n // DUPLICATE_SUBSCRIPTION. Request-level, not a session close.\n if (this._draftVersion !== 14 && !isDraft22(this._draftVersion) && sub.trackKey !== undefined\n && (this.findLiveIncomingByTrack(sub.trackKey, /* publishInitiated */ false)\n || this.findLiveOutboundPublishByTrack(sub.trackKey))) {\n const errorMsg: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId: msg.requestId,\n errorCode: RequestErrorCode.DUPLICATE_SUBSCRIPTION,\n retryInterval: varint(0n),\n errorReason: 'Duplicate subscription: a publisher-role subscription for this track already exists (§5.1)',\n };\n return [this.sendControl(errorMsg), ...(validated.replenish ?? [])];\n }\n\n // §9.2.2.5: store the subscriber's filter type — the Joining Fetch gate\n // (§9.16.2) needs it. An omitted parameter = unfiltered (NOT Largest\n // Object). Malformed filter bytes were already rejected by the message\n // parameter validation before this handler runs; a decode failure here is\n // treated the same as an omitted filter rather than crashing the session.\n const filterValues = msg.parameters.get(MessageParam.SUBSCRIPTION_FILTER);\n const filterBytes = filterValues?.[0];\n if (filterBytes instanceof Uint8Array) {\n sub.currentFilter = filterBytes;\n try {\n sub.setRemoteFilterType(decodeSubscriptionFilter(filterBytes, this._draftVersion).type);\n } catch { /* malformed — leave unfiltered */ }\n }\n\n // §9.2.2.8: FORWARD on the SUBSCRIBE sets the initial forward state\n // (0 = paused). draft-18 §10.12.2 gates Joining Fetches on this.\n const forwardValues = msg.parameters.get(MessageParam.FORWARD);\n const forwardVal = forwardValues?.[0];\n if (typeof forwardVal === 'bigint') {\n sub.setInitialForwardState(forwardVal === 0n ? ForwardState.PAUSED : ForwardState.ACTIVE);\n }\n const order = msg.parameters.get(MessageParam.GROUP_ORDER)?.[0];\n if (order !== undefined) sub.requestedGroupOrder = order === 2n ? 'descending' : 'ascending';\n\n this.incomingSubscriptions.set(msg.requestId as bigint, sub);\n\n return validated.replenish ?? [];\n }\n\n /**\n * Handle incoming PUBLISH from a publisher (publisher-initiated subscription).\n * Draft-14 §9.13: The publisher sends PUBLISH to indicate it wants to\n * publish on a track. The subscriber creates state to track this and\n * the application responds via acceptSubscribe() or rejectSubscribe().\n * @see draft-ietf-moq-transport-14 §9.13\n */\n private handleIncomingPublish(msg: Publish): SessionOutboundAction[] {\n // §2.4.1: validate the Full Track Name BEFORE registering an alias or state.\n const nameError = this.validateFullName18(msg.trackNamespace, msg.trackName);\n if (nameError) return nameError;\n\n // Validate incoming request ID and auto-replenish MAX_REQUEST_ID §9.5\n const validated = this.validateAndReplenish(msg.requestId);\n if (validated.error) return validated.error;\n\n // Create the subscription state machine flagged as PUBLISH-initiated (this\n // also computes its track key). NOT stored / alias NOT registered until the\n // duplicate checks below pass.\n const sub = SubscriptionStateMachine.createFromPublish(\n msg.requestId,\n msg.trackNamespace,\n msg.trackName,\n msg.parameters,\n msg.trackAlias as bigint,\n );\n // §10.10 / §5.1: the initiator sets the initial Forward State in PUBLISH —\n // apply it BEFORE the subscription is stored, so both endpoints agree\n // (the subscriber's acceptance may still override it).\n const publishForward = msg.parameters.get(MessageParam.FORWARD)?.[0];\n if (typeof publishForward === 'bigint') {\n sub.setInitialForwardState(publishForward === 0n ? ForwardState.PAUSED : ForwardState.ACTIVE);\n }\n if (isDraft22(this._draftVersion)) {\n this.initializePublishPolicy22(sub, msg.parameters, msg.trackProperties);\n }\n\n // §5.1: an endpoint may hold at most ONE subscription per Track per ROLE\n // (drafts 16/18 only — draft-14 defines neither this rule nor\n // DUPLICATE_SUBSCRIPTION 0x19, and draft 22 §3.1 allows concurrent\n // subscriptions). A PUBLISH makes US the subscriber. Checked BEFORE\n // touching the alias registry.\n if (this._draftVersion !== 14 && !isDraft22(this._draftVersion) && sub.trackKey !== undefined) {\n const outboundSub = this.findLiveOutboundSubscribeByTrack(sub.trackKey);\n // (a) A committed subscriber-role subscription — an earlier PUBLISH we\n // accepted, or a local ESTABLISHED SUBSCRIBE — is a duplicate: reject.\n if (this.findLiveIncomingByTrack(sub.trackKey, /* publishInitiated */ true)\n || (outboundSub && outboundSub.state === 'established')) {\n return [this.buildPublishReject(\n msg.requestId, RequestErrorCode.DUPLICATE_SUBSCRIPTION,\n 'Duplicate subscription: a subscriber-role subscription for this track already exists (§5.1)',\n ), ...(validated.replenish ?? [])];\n }\n // (b) A local PENDING SUBSCRIBE for the same track: §5.1 requires the\n // subscription WE initiated to reach Terminated BEFORE we send\n // PUBLISH_OK — but ONLY if we accept the PUBLISH. STAGE the cancellation\n // (keyed by this PUBLISH's request ID) and accept the PUBLISH into\n // Pending state; acceptSubscribe performs it, and a rejected / malformed\n // / torn-down PUBLISH clears it, leaving the local SUBSCRIBE alive.\n if (outboundSub && outboundSub.state === 'pending') {\n this.pendingCollisions.set(msg.requestId as bigint, outboundSub.requestId as bigint);\n }\n }\n\n // Draft-14 §9.13: \"The same Track Alias MUST NOT be used to refer to two\n // different Tracks simultaneously. If a subscriber receives a PUBLISH that\n // uses the same Track Alias as a different track with an active subscription,\n // it MUST close the session with error DUPLICATE_TRACK_ALIAS.\"\n try {\n this.trackAliases.register(msg.trackAlias, msg.trackNamespace, msg.trackName, msg.requestId as bigint);\n } catch {\n return this.closeWithError(\n SessionErrorCode.DUPLICATE_TRACK_ALIAS,\n `Duplicate track alias ${msg.trackAlias} in PUBLISH`,\n );\n }\n\n this.incomingSubscriptions.set(msg.requestId as bigint, sub);\n\n return validated.replenish ?? [];\n }\n\n /**\n * Find a live (non-terminated) incoming subscription for a Full Track Name in\n * the given ROLE (publisher = peer SUBSCRIBE we serve; subscriber = peer\n * PUBLISH we receive). Used to enforce §5.1's one-subscription-per-role rule.\n */\n private findLiveIncomingByTrack(trackKey: string, publishInitiated: boolean): SubscriptionStateMachine | undefined {\n for (const sub of this.incomingSubscriptions.values()) {\n if (sub.trackKey === trackKey && sub.isPublishInitiated === publishInitiated && !sub.isTerminated) {\n return sub;\n }\n }\n return undefined;\n }\n\n /**\n * Find a live (non-terminated) LOCAL outbound SUBSCRIBE (we are the subscriber)\n * for a Full Track Name. Part of the §5.1 one-per-role check: a local SUBSCRIBE\n * and an inbound PUBLISH for the same track both put us in the subscriber role.\n */\n private findLiveOutboundSubscribeByTrack(trackKey: string): SubscriptionStateMachine | undefined {\n for (const sub of this.subscriptions.values()) {\n if (sub.trackKey === trackKey && !sub.isTerminated) return sub;\n }\n return undefined;\n }\n\n /**\n * Find a live (non-terminated) LOCAL outbound PUBLISH (we are the publisher)\n * for a Full Track Name. Part of the §5.1 one-per-role check: a local PUBLISH\n * and an inbound SUBSCRIBE for the same track both put us in the publisher role.\n */\n private findLiveOutboundPublishByTrack(trackKey: string): SubscriptionStateMachine | undefined {\n for (const sub of this.outgoingPublishes.values()) {\n if (sub.trackKey === trackKey && !sub.isTerminated) return sub;\n }\n return undefined;\n }\n\n private assertPublisherAliasAvailable(alias: bigint, namespace: Uint8Array[], name: Uint8Array): void {\n if (!isDraft22(this._draftVersion)) return;\n for (const requests of [this.incomingSubscriptions, this.outgoingPublishes]) {\n for (const sub of requests.values()) {\n if (!sub.isPublisher || sub.isTerminated || sub.trackAlias !== alias) continue;\n const sameTrack = sub.trackNamespace?.length === namespace.length\n && prefixesOverlap(sub.trackNamespace, namespace)\n && sub.trackName?.length === name.length\n && sub.trackName.every((byte, index) => byte === name[index]);\n if (!sameTrack) throw new SessionError(`Track alias ${alias} already belongs to a different track`, 'INVALID_STATE');\n }\n }\n }\n\n private initializePublishPolicy22(sub: SubscriptionStateMachine, parameters: Parameters, properties: TrackProperties | undefined): void {\n const filter = parameters.get(MessageParam.SUBSCRIPTION_FILTER)?.[0];\n if (filter instanceof Uint8Array) sub.currentFilter = filter;\n const order = parameters.get(MessageParam.GROUP_ORDER)?.[0];\n if (order !== undefined) sub.requestedGroupOrder = order === 2n ? 'descending' : 'ascending';\n sub.publisherGroupOrder = properties?.get(0x22n)?.[0] === 2n ? 'descending' : 'ascending';\n sub.locationWindow = subscriptionWindow(\n sub.currentFilter === undefined ? undefined : decodeSubscriptionFilter(sub.currentFilter, 22),\n this.extractLargestObjectParam(parameters) ?? undefined,\n );\n }\n\n /**\n * §5.1: perform a STAGED cancellation of a local SUBSCRIBE superseded by an\n * inbound PUBLISH, at the moment we accept that PUBLISH (before PUBLISH_OK). No\n * staged entry (rejected/torn-down PUBLISH, or no collision) → no actions, so the\n * local SUBSCRIBE survives. Otherwise terminate it and DELETE its state; a benign\n * terminal crossing the cancellation is tolerated in O(1) via the allocation\n * frontier. Returns the cancellation actions to PREPEND before the PUBLISH_OK.\n */\n private cancelStagedCollision(publishRequestId: bigint): SessionOutboundAction[] {\n const localReqId = this.pendingCollisions.get(publishRequestId as bigint);\n if (localReqId === undefined) return [];\n this.pendingCollisions.delete(publishRequestId as bigint);\n const sub = this.subscriptions.get(localReqId);\n if (!sub || sub.isTerminated) return []; // already gone (app unsubscribed, etc.)\n\n // Reclaim its full state (delete the SM + track metadata + release its alias).\n this.terminateLocalSubscriberRequest(sub);\n\n // Adapter/wire cancellation: a `cancel_request` (reject the pending\n // subscribeTrack(); draft-18 reset the request stream), preceded on draft-14/16\n // by an UNSUBSCRIBE on the wire.\n const cancel: SessionOutboundAction = {\n type: 'cancel_request',\n requestId: localReqId,\n reason: 'local SUBSCRIBE superseded by a peer PUBLISH for the same track (§5.1)',\n };\n if (isRequestStreamDraft(this._draftVersion)) return [cancel];\n return [this.sendControl({ type: 'UNSUBSCRIBE', requestId: localReqId } as Unsubscribe), cancel];\n }\n\n /**\n * Locally terminate a subscriber-side subscription (public UNSUBSCRIBE or §5.1\n * supersession) and RECLAIM its full state: terminate the SM, release its alias,\n * and delete the SM + track metadata. Records compact cancellation PROVENANCE\n * (id → 'subscribe') so exactly one legitimate crossed terminal is tolerated —\n * bounded, one-shot, never closing the session on churn.\n */\n private terminateLocalSubscriberRequest(sub: SubscriptionStateMachine): void {\n const requestId = sub.requestId;\n for (const fillId of this.expectedFillsOf(requestId)) this.cancelFillRequest(fillId);\n // Capture the phase BEFORE superseding: if it was already ESTABLISHED, no crossed\n // SUBSCRIBE_OK is legal (§5.1), so the shadow must start ESTABLISHED. (At cancel\n // time the SM is PENDING or ESTABLISHED — never yet TERMINATED.)\n const established = !sub.isPending;\n if (!sub.isTerminated) sub.supersede(); // PENDING or ESTABLISHED → TERMINATED\n if (sub.trackAlias !== undefined) this.trackAliases.unregister(sub.trackAlias, requestId);\n this.subscriptions.delete(requestId as bigint);\n this.subscriptionTracks.delete(requestId as bigint);\n this.recordCancellation(requestId, 'subscribe', established);\n }\n\n /**\n * Record that WE cancelled `requestId` — a shadow that tolerates the crossed peer\n * SEQUENCE for it (see {@link recentlyCancelled}). `established` sets the starting\n * phase: an already-established request expects no further OK.\n */\n private recordCancellation(requestId: bigint, kind: 'subscribe' | 'fetch', established: boolean): void {\n // LOSSLESS: no cap, no eviction. A crossing may be arbitrarily delayed on the\n // shared draft-14/16 control stream, so forgetting a shadow would false-close a\n // valid session; the record is kept until its crossing consumes it (or, draft-18,\n // the request-stream close reclaims it). See {@link recentlyCancelled}.\n this.recentlyCancelled.delete(requestId as bigint); // refresh insertion recency\n this.recentlyCancelled.set(requestId as bigint, { kind, phase: established ? 'established' : 'pending' });\n }\n\n /**\n * A crossed NON-terminal OK. For a SUBSCRIBE (SUBSCRIBE_OK) it is legal only from\n * PENDING and transitions the shadow to ESTABLISHED, KEEPING it (a PUBLISH_DONE\n * terminal may still follow). For a FETCH (FETCH_OK) there is NO legal further\n * control response (§5.2: the terminal is the data-stream FIN, not a message), so\n * it is ONE-SHOT — reclaim immediately. Returns false (→ §9.1 close) for the wrong\n * kind, a request we never cancelled, or a duplicate OK (already ESTABLISHED).\n */\n private toleratesCrossedOk(requestId: bigint, kind: 'subscribe' | 'fetch'): boolean {\n const shadow = this.recentlyCancelled.get(requestId as bigint);\n if (!shadow || shadow.kind !== kind || shadow.phase !== 'pending') return false;\n if (kind === 'fetch') this.recentlyCancelled.delete(requestId as bigint); // one-shot\n else shadow.phase = 'established';\n return true;\n }\n\n /**\n * A crossed TERMINAL. PUBLISH_DONE (subscribe) is legal from EITHER phase and\n * reclaims the shadow. REQUEST_ERROR (either kind) is legal ONLY from the PENDING\n * phase (§5.1/§5.2: no error after an OK) — after an OK it is a violation. Returns\n * true (reclaimed) if legal; false (→ §9.1 close) otherwise.\n */\n private consumeCrossedTerminal(\n requestId: bigint,\n terminal: 'publish_done' | 'request_error',\n ...kinds: ('subscribe' | 'fetch')[]\n ): boolean {\n const shadow = this.recentlyCancelled.get(requestId as bigint);\n if (!shadow || !kinds.includes(shadow.kind)) return false;\n // A REQUEST_ERROR after the OK-or-error response was already given is illegal.\n if (terminal === 'request_error' && shadow.phase === 'established') return false;\n this.recentlyCancelled.delete(requestId as bigint);\n return true;\n }\n\n /**\n * Whether a control response for `requestId` is a benign crossed message for a\n * request WE cancelled — it has a live exact shadow. The I/O layer uses this to\n * keep the application `onMessage` contract consistent — such a response is not\n * surfaced as an event (qlog still observes it raw). Read-only peek; matches the\n * tolerate/close decision in the handlers so suppression and tolerance never diverge.\n */\n isCancelledRequest(requestId: bigint): boolean {\n return this.recentlyCancelled.has(requestId as bigint);\n }\n\n /** Build the request-level rejection for an incoming PUBLISH. draft-14 uses\n * PUBLISH_ERROR; drafts 16/18 use the generic REQUEST_ERROR (PUBLISH_ERROR is a\n * draft-14-only type — unencodable on draft-16). On draft-16 the REQUEST_ERROR\n * rides the control stream; on draft-18 the adapter writes it on the PUBLISH\n * request stream. The draft-14 field is a QUIC Varint — range-guard errorCode. */\n private buildPublishReject(requestId: bigint, errorCode: bigint, errorReason: string): SessionOutboundAction {\n if (this._draftVersion === 14) {\n return this.sendControl({\n type: 'PUBLISH_ERROR', requestId, errorCode: varint(errorCode), errorReason,\n } as PublishError);\n }\n return this.sendControl({\n type: 'REQUEST_ERROR', requestId, errorCode, retryInterval: varint(0n), errorReason,\n } as RequestErrorMsg);\n }\n\n private handleIncomingFetch(msg: Fetch): SessionOutboundAction[] {\n // §2.4.1: a standalone FETCH carries a Full Track Name — validate it BEFORE any\n // state. (A joining FETCH references an existing request and carries no name.)\n if (msg.fetch.fetchType === 0x1) {\n const sf = msg.fetch as StandaloneFetch;\n const nameError = this.validateFullName18(sf.trackNamespace, sf.trackName);\n if (nameError) return nameError;\n }\n\n // Validate incoming request ID and auto-replenish MAX_REQUEST_ID §9.5\n const validated = this.validateAndReplenish(msg.requestId);\n if (validated.error) return validated.error;\n\n const filterError = this.rejectUnsupportedRangeFilters(msg.requestId, msg.parameters);\n if (filterError) return filterError;\n\n // Joining Fetch (0x2/0x3): enforce the MUSTs decidable from protocol state\n // BEFORE creating any fetch state (§9.16.2 / draft-18 §10.12.2).\n if (msg.fetch.fetchType !== 0x1) {\n const jf = msg.fetch as JoiningFetch;\n const joiningReqId = jf.joiningRequestId as bigint;\n // A subscription this publisher serves may have been created by the\n // peer's SUBSCRIBE (incomingSubscriptions) OR by our own outbound\n // PUBLISH (outgoingPublishes) — d18 §5.1 saves the PUBLISH-communicated\n // largest precisely so joins against it can resolve.\n const sub = this.incomingSubscriptions.get(joiningReqId);\n const pubSub = sub === undefined ? this.outgoingPublishes.get(joiningReqId) : undefined;\n const joined = sub ?? pubSub;\n\n // Eligibility matrix (§9.16.2 / d18 §10.12.2):\n // - draft-14: an EXISTING (established) subscription only;\n // - drafts 16/18: Established, or Pending SUBSCRIBER-initiated (a\n // peer SUBSCRIBE we serve) — a PENDING publisher-initiated PUBLISH\n // is invalid until PUBLISH_OK;\n // - an inbound PUBLISH we consume is never joinable BY the peer.\n const eligible = (() => {\n if (joined === undefined) return false;\n if (sub !== undefined && sub.isPublishInitiated) return false; // we are the subscriber there\n if (joined.state === SubscriptionState.ESTABLISHED) return true;\n if (this._draftVersion === 14) return false; // d14: established only\n // Pending: subscriber-initiated only (peer SUBSCRIBE, never our PUBLISH).\n return joined.state === SubscriptionState.PENDING && sub !== undefined;\n })();\n if (!eligible) {\n const errorMsg: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId: msg.requestId,\n // draft-14 has no generic REQUEST_ERROR: this encodes as\n // FETCH_ERROR (0x19) with the d14 code INVALID_JOINING_REQUEST_ID\n // (0x7, §9.18); drafts 16/18 use the common-table 0x32.\n errorCode: this._draftVersion === 14 ? varint(0x7n) : RequestErrorCode.INVALID_JOINING_REQUEST_ID,\n retryInterval: varint(0n),\n errorReason: `Joining Fetch references request ${joiningReqId}, which is not a subscription in PENDING/ESTABLISHED state (§9.16.2)`,\n requestKind: 'FETCH', // draft-14 → FETCH_ERROR; ignored by 16/18\n };\n return [this.sendControl(errorMsg), ...(validated.replenish ?? [])];\n }\n\n if (isRequestStreamDraft(this._draftVersion)) {\n // draft-18 §10.12.2: \"A Joining Fetch is only permitted when the\n // associated subscription has Forward State 1; otherwise the publisher\n // MUST respond with a REQUEST_ERROR with error code INVALID_RANGE.\"\n // The gate is evaluated ONLY for an ESTABLISHED subscription: a\n // PENDING one defers to the buffering layer, which re-evaluates at\n // establish time — after any pending REQUEST_UPDATEs have applied.\n if (joined!.state === SubscriptionState.ESTABLISHED\n && joined!.forwardState !== ForwardState.ACTIVE) {\n const errorMsg: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId: msg.requestId,\n errorCode: RequestErrorCode.INVALID_RANGE,\n retryInterval: varint(0n),\n errorReason: `Joining Fetch on subscription ${joiningReqId} with Forward State 0 (§10.12.2)`,\n requestKind: 'FETCH', // draft-18-only branch; kind stamped for uniformity\n };\n return [this.sendControl(errorMsg), ...(validated.replenish ?? [])];\n }\n } else {\n // §9.16.2: \"A Joining Fetch is only permitted when the associated\n // Subscribe has the Filter Type Largest Object; any other value\n // results in closing the session with a PROTOCOL_VIOLATION.\" An\n // omitted SUBSCRIPTION_FILTER = unfiltered (§9.2.2.5) — not Largest\n // Object, so it fails this gate too.\n // §9.16.2's Largest Object FILTER rule binds regardless of who\n // initiated the subscription: a SUBSCRIBE communicates the filter in\n // its request; a PUBLISH-initiated subscription gets it from the\n // subscriber's PUBLISH_OK (§9.2.2.5). Unfiltered fails the gate.\n if (joined!.remoteFilterType !== 'LargestObject') {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `Joining Fetch on subscription ${joiningReqId} whose filter is ${joined!.remoteFilterType ?? 'unfiltered'}, not Largest Object (§9.16.2)`,\n );\n }\n }\n\n const fetchSm = FetchStateMachine.createAsJoiningPublisher(msg.requestId, {\n fetchType: msg.fetch.fetchType,\n joiningRequestId: joiningReqId,\n joiningStart: jf.joiningStart,\n });\n this.incomingFetches.set(msg.requestId as bigint, fetchSm);\n\n return validated.replenish ?? [];\n }\n\n // Standalone fetch (0x1): the range comes from the message.\n const sf = msg.fetch as StandaloneFetch;\n // §3.2: reserved `.`/`.session` namespace → REQUEST_ERROR DOES_NOT_EXIST.\n const reserved = this.rejectReservedNamespace(sf.trackNamespace, msg.requestId, validated.replenish);\n if (reserved) return reserved;\n\n const fetchSm = FetchStateMachine.createAsPublisher(\n msg.requestId,\n sf.startLocation.group,\n sf.startLocation.object,\n sf.endLocation.group,\n sf.endLocation.object,\n );\n this.incomingFetches.set(msg.requestId as bigint, fetchSm);\n\n return validated.replenish ?? [];\n }\n\n /**\n * The saved Joining Location per incoming subscription (§5.1): the Largest\n * Location this publisher communicated in SUBSCRIBE_OK (or, on draft-18, in\n * a REQUEST_UPDATE_OK that changed the Forward State 0→1). Authoritative\n * for Joining-FETCH range arithmetic; reclaimed with the subscription.\n */\n private readonly incomingJoiningLocations = new Map<bigint, { group: bigint; object: bigint }>();\n\n /**\n * Application hook supplying the publisher's CURRENT Largest Location for a\n * track (the session never sees published objects). draft-18 §5.1 requires\n * a REQUEST_UPDATE_OK that flips Forward 0→1 to communicate — and this\n * publisher to save — the current head as the new Joining Location; without\n * this hook a paused track that advanced would resume with a stale one.\n */\n private largestLocationProvider: ((subscriptionRequestId: bigint) => { group: bigint; object: bigint } | null) | null = null;\n\n setLargestLocationProvider(provider: (subscriptionRequestId: bigint) => { group: bigint; object: bigint } | null): void {\n this.largestLocationProvider = provider;\n }\n\n /** Parse a LARGEST_OBJECT parameter value: d14/16 Location byte blob, or a\n * d18 typed {group, object}. Null when absent/malformed. */\n private extractLargestObjectParam(parameters: Parameters | undefined): { group: bigint; object: bigint } | null {\n const raw = parameters?.get(MessageParam.LARGEST_OBJECT as bigint)?.[0];\n if (raw instanceof Uint8Array) {\n try {\n const { value } = readLocation(raw, 0);\n return { group: BigInt(value.group), object: BigInt(value.object) };\n } catch { return null; }\n }\n if (raw !== null && raw !== undefined && typeof raw === 'object'\n && 'group' in (raw as object) && 'object' in (raw as object)) {\n const loc = raw as { group: bigint | number; object: bigint | number };\n return { group: BigInt(loc.group), object: BigInt(loc.object) };\n }\n return null;\n }\n\n private handleFetchCancel(msg: FetchCancel): SessionOutboundAction[] {\n const fetch = this.incomingFetches.get(msg.requestId as bigint);\n if (fetch) {\n // draft-16 §9.18: the fetcher may cancel from PENDING or TRANSFERRING. Mark it\n // cancelled and RECLAIM the incoming-fetch state (bounded — a peer cannot grow\n // incomingFetches with valid unique cancelled requests). The I/O layer aborts\n // any response stream it had opened for this fetch.\n fetch.handleFetchCancel();\n this.incomingFetches.delete(msg.requestId as bigint);\n return [];\n }\n\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${msg.requestId} for FETCH_CANCEL`,\n );\n }\n\n /**\n * Handle incoming REQUEST_UPDATE from subscriber (publisher-side).\n * §9.11: \"The receiver MUST close the session with PROTOCOL_VIOLATION\n * if the sender specifies an invalid Existing Request ID.\"\n * §9.11: \"The receiver of a REQUEST_UPDATE MUST respond with exactly one\n * REQUEST_OK or REQUEST_ERROR.\"\n */\n private handleIncomingRequestUpdate(msg: RequestUpdate): SessionOutboundAction[] {\n // Validate the new request ID and auto-replenish MAX_REQUEST_ID §9.5\n const validated = this.validateAndReplenish(msg.requestId);\n if (validated.error) return validated.error;\n\n // draft-18 §10.9.2: a peer prefix update on an inbound SUBSCRIBE_NAMESPACE /\n // SUBSCRIBE_TRACKS we serve (separate maps — handle before the generic lookup,\n // which would otherwise treat the Existing Request ID as unknown and close).\n if (this.incomingNamespaceSubscriptions.has(msg.existingRequestId as bigint)\n || this.incomingTrackSubscriptions.has(msg.existingRequestId as bigint)) {\n return this.handleIncomingPrefixUpdate(msg, validated.replenish);\n }\n\n // draft-18 §10.2.14: TRACK_NAMESPACE_PREFIX is in scope for REQUEST_UPDATE\n // ONLY when it targets a SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS (handled\n // above). On a REQUEST_UPDATE for a normal subscription/fetch/publish it is\n // out of scope → PROTOCOL_VIOLATION.\n if (isRequestStreamDraft(this._draftVersion) && msg.parameters.has(MessageParam.TRACK_NAMESPACE_PREFIX as bigint)) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `TRACK_NAMESPACE_PREFIX is out of scope for a REQUEST_UPDATE that does not target a SUBSCRIBE_NAMESPACE/SUBSCRIBE_TRACKS (§10.2.14)`,\n );\n }\n\n // §9.11: Look up Existing Request ID — must match an active subscription we\n // serve, an OUTBOUND PUBLISH we initiated (draft-18 §10.9, a peer update on\n // our PUBLISH stream), a fetch, or an inbound PUBLISH_NAMESPACE we accepted.\n // An outbound PUBLISH is a publisher-side subscription, so it follows the\n // same established-state / FORWARD / REQUEST_OK path as an inbound one.\n const sub = this.incomingSubscriptions.get(msg.existingRequestId as bigint)\n ?? this.outgoingPublishes.get(msg.existingRequestId as bigint);\n const fetch = this.incomingFetches.get(msg.existingRequestId as bigint);\n const pubNs = this.incomingPublishNamespaces.get(msg.existingRequestId as bigint);\n\n if (!sub && !fetch && !pubNs) {\n // §9.11: \"MUST close the session with PROTOCOL_VIOLATION if the sender\n // specifies an invalid Existing Request ID\"\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `REQUEST_UPDATE references unknown Existing Request ID ${msg.existingRequestId}`,\n );\n }\n\n // draft-18 §10.9: a PUBLISH_NAMESPACE may be updated on its request stream.\n // We have no per-namespace mutable state to apply yet — acknowledge with\n // REQUEST_OK (or, for v14, no response). Params are accepted as-is.\n const contextError = this.validateUpdateContext22(msg.parameters, sub ? 'SUBSCRIBE' : fetch ? 'FETCH' : 'PUBLISH_NAMESPACE');\n if (contextError) return contextError;\n const filterError = this.rejectUnsupportedRangeFilters(msg.requestId, msg.parameters);\n if (filterError) return filterError;\n if (pubNs && !sub && !fetch) {\n if (this._draftVersion === 14) return validated.replenish ?? [];\n const requestOk: RequestOk = { type: 'REQUEST_OK', requestId: msg.requestId, parameters: new Map() };\n return [this.sendControl(requestOk), ...(validated.replenish ?? [])];\n }\n\n // Must be ESTABLISHED to accept updates — except draft-18 SUBSCRIBE, where\n // updates may race establishment (§10.12.2 \"process any pending\n // REQUEST_UPDATE messages ... before evaluating\"). The exception does NOT\n // cover publish-initiated subscriptions: an update acknowledgement before\n // PUBLISH_OK has ambiguous correlation on the publish stream.\n if (sub && sub.state !== 'established'\n && !(isRequestStreamDraft(this._draftVersion) && sub.state === 'pending' && !sub.isPublishInitiated)) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `REQUEST_UPDATE for subscription ${msg.existingRequestId} in state ${sub.state}; expected established`,\n );\n }\n if (fetch && fetch.state !== 'transferring') {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `REQUEST_UPDATE for fetch ${msg.existingRequestId} in state ${fetch.state}; expected transferring`,\n );\n }\n\n // §9.11: \"The receiver MUST close the session with PROTOCOL_VIOLATION\n // if the parameters included in the REQUEST_UPDATE are invalid for the\n // type of request being modified.\"\n // Parameters valid only for subscription REQUEST_UPDATEs (not fetches):\n // - FORWARD (§9.2.2.8), SUBSCRIPTION_FILTER (§9.2.2.5), NEW_GROUP_REQUEST (§9.2.2.9)\n if (fetch) {\n const subscriptionOnlyParams = [\n MessageParam.FORWARD,\n MessageParam.SUBSCRIPTION_FILTER,\n MessageParam.NEW_GROUP_REQUEST,\n ];\n for (const param of subscriptionOnlyParams) {\n if (msg.parameters.has(param)) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `Parameter 0x${(param as bigint).toString(16)} is not valid for REQUEST_UPDATE on a fetch (§9.11)`,\n );\n }\n }\n }\n\n // §9.11: \"If a parameter previously set on the request is not present\n // in REQUEST_UPDATE, its value remains unchanged.\"\n const prevForwardState = sub?.forwardState;\n const forwardValues = msg.parameters.get(MessageParam.FORWARD);\n // draft-18 §5.1: a REQUEST_UPDATE_OK that flips Forward 0→1 MUST\n // communicate (and this publisher save) the CURRENT Largest Location.\n // The lookup completes BEFORE any Forward mutation, and every\n // can't-comply shape fails CLOSED (REQUEST_ERROR, Forward unchanged):\n // - no provider installed → the publisher cannot meet the MUST;\n // - provider throws → likewise;\n // - invalid result shape → likewise;\n // - provider returns null → compliant: the track has no objects, the\n // OK carries no LARGEST_OBJECT and any STALE saved Joining Location\n // is cleared (a later join is INVALID_RANGE, never anchored stale).\n let resumeLocation: { group: bigint; object: bigint } | null = null;\n let resumeClearsStale = false;\n const isSubscriptionScope = sub !== undefined && !fetch\n && (this.incomingSubscriptions.has(msg.existingRequestId as bigint)\n || this.outgoingPublishes.has(msg.existingRequestId as bigint));\n // Scope: an ESTABLISHED subscription resuming. §5.1's clause is\n // unqualified, but for a PENDING subscription no Joining Location has\n // been communicated yet and the acknowledgement that will (SUBSCRIBE_OK)\n // is still outstanding — d18 §10.12.2 explicitly orders pending\n // REQUEST_UPDATEs BEFORE establishment evaluation, so the SUBSCRIBE_OK\n // emitted after them carries the current largest and satisfies the save\n // requirement for the resumed state. Failing such an update closed here\n // would reject the very races §10.12.2 requires publishers to tolerate.\n const wantsResume = isRequestStreamDraft(this._draftVersion) && isSubscriptionScope\n && sub!.state === 'established'\n && prevForwardState === ForwardState.PAUSED\n && forwardValues !== undefined && forwardValues.length > 0\n && typeof forwardValues[0] === 'bigint' && forwardValues[0] !== 0n;\n if (wantsResume || (isDraft22(this._draftVersion) && isSubscriptionScope)) {\n // d18 §10.11: \"PUBLISH_DONE ... UPDATE_FAILED\": an unsuccessful\n // subscription update terminates the subscription. The SESSION emits\n // only the REQUEST_ERROR here — the I/O layer owns the termination\n // transaction (it must close in-flight publisher operations, report a\n // truthful Stream Count, retire the alias, and seal the request\n // stream, none of which bare Session actions can do). The adapter\n // detects the REQUEST_ERROR update response and runs its publishDone.\n const failClosed = (why: string): SessionOutboundAction[] => {\n const reason = `cannot update subscription ${msg.existingRequestId}: ${why} (REQUEST_UPDATE_OK requires the current Largest Location)`;\n return [this.sendControl({\n type: 'REQUEST_ERROR',\n requestId: msg.requestId,\n errorCode: varint(0x0n), // INTERNAL_ERROR\n retryInterval: varint(0n),\n errorReason: reason,\n } as RequestErrorMsg), ...(validated.replenish ?? [])];\n };\n if (!this.largestLocationProvider) {\n return failClosed('no Largest Location provider is installed');\n }\n let provided: { group: bigint; object: bigint } | null;\n try {\n provided = this.largestLocationProvider(msg.existingRequestId as bigint);\n } catch (err) {\n return failClosed(`the Largest Location provider threw (${err instanceof Error ? err.message : String(err)})`);\n }\n if (provided === null) {\n resumeClearsStale = true;\n } else if (typeof provided === 'object' && provided !== null\n && typeof provided.group === 'bigint' && typeof provided.object === 'bigint'\n && provided.group >= 0n && provided.object >= 0n\n && provided.group <= MAX_VI64 && provided.object <= MAX_VI64) {\n resumeLocation = { group: provided.group, object: provided.object };\n } else {\n return failClosed('the Largest Location provider returned an invalid value');\n }\n }\n // Apply FORWARD parameter if present\n if (forwardValues && forwardValues.length > 0 && sub) {\n const forwardVal = forwardValues[0];\n if (typeof forwardVal === 'bigint') {\n const next = forwardVal === 0n ? ForwardState.PAUSED : ForwardState.ACTIVE;\n // PENDING (d18 update racing establishment): the setter that works\n // pre-establish; ESTABLISHED: the normal transition.\n if (sub.state === 'pending') sub.setInitialForwardState(next);\n else sub.updateForwardState(next);\n }\n }\n\n // §9.2.2.5: SUBSCRIPTION_FILTER MAY appear in REQUEST_UPDATE — the stored\n // remote filter must track it, or the Joining Fetch gate (§9.16.2) tests\n // a stale value: LargestObject→AbsoluteStart would keep permitting joins,\n // and NextGroupStart→LargestObject would wrongly close the session.\n // Omitted ⇒ unchanged (per the same section).\n const updatedFilter = msg.parameters.get(MessageParam.SUBSCRIPTION_FILTER)?.[0];\n if (updatedFilter instanceof Uint8Array && sub) {\n sub.currentFilter = updatedFilter;\n try {\n sub.setRemoteFilterType(decodeSubscriptionFilter(updatedFilter, this._draftVersion).type);\n } catch { /* malformed bytes were already rejected by parameter validation */ }\n }\n if (isDraft22(this._draftVersion) && isSubscriptionScope && sub && updatedFilter instanceof Uint8Array) {\n sub.locationWindow = subscriptionWindow(\n sub.currentFilter === undefined ? undefined : decodeSubscriptionFilter(sub.currentFilter, 22),\n resumeLocation ?? undefined,\n );\n }\n\n // §8: OBJECT/SUBGROUP_DELIVERY_TIMEOUT MAY appear in REQUEST_UPDATE — the receiver\n // records the peer's new value on the subscription (omitted ⇒ unchanged, §9.11).\n if (sub) {\n const objTimeout = msg.parameters.get(MessageParam.OBJECT_DELIVERY_TIMEOUT)?.[0];\n if (typeof objTimeout === 'bigint') sub.requestedDeliveryTimeoutMs = Number(objTimeout);\n const subgroupTimeout = msg.parameters.get(MessageParam.SUBGROUP_DELIVERY_TIMEOUT)?.[0];\n if (typeof subgroupTimeout === 'bigint') sub.requestedSubgroupDeliveryTimeoutMs = Number(subgroupTimeout);\n }\n\n // Draft-14 §9.10: \"There is no control message in response to a\n // SUBSCRIBE_UPDATE, because it is expected that it will always succeed.\"\n if (this._draftVersion === 14) {\n return validated.replenish ?? [];\n }\n\n // Communicate + SAVE the pre-validated resume location (see above).\n const requestOkParams: Parameters = new Map();\n if (resumeLocation) {\n requestOkParams.set(MessageParam.LARGEST_OBJECT as bigint, [resumeLocation]);\n this.incomingJoiningLocations.set(msg.existingRequestId as bigint, resumeLocation);\n } else if (resumeClearsStale) {\n this.incomingJoiningLocations.delete(msg.existingRequestId as bigint);\n }\n\n // Draft-16 §9.11: Respond with REQUEST_OK\n const requestOk: RequestOk = {\n type: 'REQUEST_OK',\n requestId: msg.requestId,\n parameters: requestOkParams,\n };\n\n return [this.sendControl(requestOk), ...(validated.replenish ?? [])];\n }\n\n /**\n * draft-18 §10.9.2: a peer REQUEST_UPDATE changing the Track Namespace Prefix of\n * an inbound SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS we serve. Validates the\n * prefix, enforces the overlap rule independently per type, and either applies\n * the new prefix (REQUEST_OK) or rejects with PREFIX_OVERLAP. A malformed prefix\n * (>32 fields / >4096 bytes) closes the session with PROTOCOL_VIOLATION.\n */\n private handleIncomingPrefixUpdate(\n msg: RequestUpdate,\n replenish: SessionOutboundAction[] | undefined,\n ): SessionOutboundAction[] {\n const existingRid = msg.existingRequestId as bigint;\n const incNs = this.incomingNamespaceSubscriptions.get(existingRid);\n const incTracks = this.incomingTrackSubscriptions.get(existingRid);\n\n // §6.1 / §10.9.2: the original SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS must get\n // its single REQUEST_OK / REQUEST_ERROR as the FIRST response, and a prefix\n // update is only permitted for an ESTABLISHED request. A REQUEST_UPDATE before\n // we have accepted the request is a PROTOCOL_VIOLATION.\n const accepted = incNs ? incNs.isActive : incTracks?.state === 'active';\n if (!accepted) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `REQUEST_UPDATE for ${incNs ? 'SUBSCRIBE_NAMESPACE' : 'SUBSCRIBE_TRACKS'} ${existingRid} before it was accepted`,\n );\n }\n\n const contextError = this.validateUpdateContext22(msg.parameters, incNs ? 'SUBSCRIBE_NAMESPACE' : 'SUBSCRIBE_TRACKS');\n if (contextError) return contextError;\n const filterError = this.rejectUnsupportedRangeFilters(msg.requestId, msg.parameters);\n if (filterError) return filterError;\n\n // §10.9: a parameter absent from REQUEST_UPDATE leaves its value unchanged.\n // Forward on SUBSCRIBE_TRACKS changes future subscriptions only (§9.20.18).\n const forward = isDraft22(this._draftVersion) && incTracks\n ? msg.parameters.get(MessageParam.FORWARD)?.[0] : undefined;\n const prefixVals = msg.parameters.get(MessageParam.TRACK_NAMESPACE_PREFIX as bigint);\n const newPrefix = prefixVals && prefixVals.length > 0 ? prefixVals[prefixVals.length - 1] : undefined;\n if (newPrefix === undefined) {\n if (incTracks && typeof forward === 'bigint') incTracks.forward = Number(forward) as ForwardStateValue;\n const ok: RequestOk = { type: 'REQUEST_OK', requestId: msg.requestId, parameters: new Map() };\n return [this.sendControl(ok), ...(replenish ?? [])];\n }\n if (!Array.isArray(newPrefix)) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n 'TRACK_NAMESPACE_PREFIX value is not a Track Namespace tuple',\n );\n }\n try {\n validateTrackNamespacePrefix(newPrefix);\n } catch (e) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n e instanceof Error ? e.message : 'Malformed Track Namespace Prefix in REQUEST_UPDATE',\n );\n }\n\n // Overlap is checked against OTHER subscriptions of the SAME type only\n // (§10.18/§10.19 independent overlap spaces), excluding this request itself.\n if (incNs) {\n for (const [rid, existing] of this.incomingNamespaceSubscriptions) {\n if (rid === existingRid || existing.isTerminated) continue;\n if (prefixesOverlap(existing.namespacePrefix, newPrefix)) {\n return this.prefixOverlapError(msg.requestId, 'SUBSCRIBE_NAMESPACE', existingRid, replenish);\n }\n }\n incNs.updatePrefix(newPrefix);\n } else if (incTracks) {\n for (const [rid, existing] of this.incomingTrackSubscriptions) {\n if (rid === existingRid) continue;\n if (prefixesOverlap(existing.trackNamespacePrefix, newPrefix)) {\n return this.prefixOverlapError(msg.requestId, 'SUBSCRIBE_TRACKS', existingRid, replenish);\n }\n }\n incTracks.trackNamespacePrefix = newPrefix;\n if (typeof forward === 'bigint') incTracks.forward = Number(forward) as ForwardStateValue;\n }\n\n const ok: RequestOk = { type: 'REQUEST_OK', requestId: msg.requestId, parameters: new Map() };\n return [this.sendControl(ok), ...(replenish ?? [])];\n }\n\n /**\n * Reject a prefix update with REQUEST_ERROR / PREFIX_OVERLAP (§10.6.2). For a\n * SUBSCRIBE_NAMESPACE, §10.9.1 requires the responder to close the bidi stream,\n * so the publisher-side state is terminated here (the I/O layer closes the\n * stream after writing the error). A SUBSCRIBE_TRACKS keeps its existing prefix.\n */\n private prefixOverlapError(\n requestId: bigint,\n kind: 'SUBSCRIBE_NAMESPACE' | 'SUBSCRIBE_TRACKS',\n existingRid: bigint,\n replenish: SessionOutboundAction[] | undefined,\n ): SessionOutboundAction[] {\n if (kind === 'SUBSCRIBE_NAMESPACE') {\n this.incomingNamespaceSubscriptions.delete(existingRid);\n }\n const err: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId,\n errorCode: RequestErrorCode.PREFIX_OVERLAP,\n retryInterval: varint(0n),\n errorReason: `Track Namespace Prefix overlaps an existing ${kind}`,\n // draft-14 → SUBSCRIBE_NAMESPACE_ERROR. SUBSCRIBE_TRACKS is draft-18-only\n // (no draft-14 wire error), so it carries no requestKind. Ignored by 16/18.\n ...(kind === 'SUBSCRIBE_NAMESPACE' ? { requestKind: 'SUBSCRIBE_NAMESPACE' as const } : {}),\n };\n return [this.sendControl(err), ...(replenish ?? [])];\n }\n\n /**\n * Handle incoming TRACK_STATUS from a potential subscriber.\n *\n * §9.19: \"The receiver of a TRACK_STATUS message treats it identically as if it\n * had received a SUBSCRIBE message, except it does not create downstream\n * subscription state or send any Objects.\"\n *\n * No SubscriptionStateMachine is created. The application responds via\n * acceptTrackStatus() or rejectTrackStatus().\n *\n * @see draft-ietf-moq-transport-16 §9.19\n */\n private handleIncomingTrackStatus(msg: TrackStatus): SessionOutboundAction[] {\n // §2.4.1: validate the Full Track Name BEFORE recording any state.\n const nameError = this.validateFullName18(msg.trackNamespace, msg.trackName);\n if (nameError) return nameError;\n\n // Validate incoming request ID and auto-replenish MAX_REQUEST_ID §9.5\n const validated = this.validateAndReplenish(msg.requestId);\n if (validated.error) return validated.error;\n\n // §3.2: reserved `.`/`.session` namespace → REQUEST_ERROR DOES_NOT_EXIST.\n const reserved = this.rejectReservedNamespace(msg.trackNamespace, msg.requestId, validated.replenish);\n if (reserved) return reserved;\n\n // §9.19: Do NOT create subscription state\n this.incomingTrackStatuses.set(msg.requestId as bigint, {\n namespace: msg.trackNamespace,\n name: msg.trackName,\n });\n\n return validated.replenish ?? [];\n }\n\n // ─── Subscription Operations ──────────────────────────────────────────\n\n /**\n * Create a new subscription.\n */\n subscribe(\n namespace: Uint8Array[],\n name: Uint8Array,\n options: SubscribeOptions = {},\n ): RequestResult {\n this.assertEstablishedOrDraining('subscribe');\n\n if (this._state === SessionState.DRAINING) {\n throw new SessionDrainingError(\n 'Cannot create new subscriptions after GOAWAY; session is DRAINING',\n this._newSessionUri ?? '',\n );\n }\n\n const requestedForward = this.normalizeLocalForwardState(options.forward, 'SUBSCRIBE');\n if (options.fill !== undefined && !isDraft22(this._draftVersion)) {\n throw new RangeError(`FILL_PARAMETERS needs draft 22; this session speaks draft ${this._draftVersion}`);\n }\n // Encode before allocating: bad local options must not leave a pending request.\n const filterBytes = options.subscriptionFilter === undefined\n ? undefined : encodeSubscriptionFilter(options.subscriptionFilter, this._draftVersion);\n const fillBytes = options.fill === undefined\n ? undefined : encodeFillParameters(options.fill.filter, options.fill.groupOrder);\n const parameters = authorizationParameters(options);\n\n const requestId = this.requestIdAllocator.allocate();\n\n const sub = SubscriptionStateMachine.createAsSubscriber(requestId, namespace, name);\n this.subscriptions.set(requestId as bigint, sub);\n this.subscriptionTracks.set(requestId as bigint, { namespace, name });\n\n // §9.2.2: Build subscription parameters from options\n if (options.deliveryTimeout !== undefined) {\n parameters.set(MessageParam.DELIVERY_TIMEOUT, [options.deliveryTimeout]);\n // Retain the requested value (ms) so the I/O layer can floor its terminal-\n // alias guard by it (§10.11) — the publisher may keep delivering for this\n // long after teardown, so the guard must outlive it.\n sub.requestedDeliveryTimeoutMs = Number(options.deliveryTimeout);\n }\n if (options.subscriberPriority !== undefined) {\n parameters.set(MessageParam.SUBSCRIBER_PRIORITY, [options.subscriberPriority]);\n // Store for draft-14 SUBSCRIBE_UPDATE replay\n sub.currentPriority = options.subscriberPriority;\n }\n if (options.groupOrder !== undefined) {\n parameters.set(MessageParam.GROUP_ORDER, [options.groupOrder]);\n sub.requestedGroupOrder = options.groupOrder === 2n ? 'descending' : 'ascending';\n }\n if (requestedForward !== undefined) {\n // §9.2.2.8: FORWARD MAY appear in SUBSCRIBE — initial forward state.\n // Both peers must agree: mirror it into OUR state machine too.\n parameters.set(MessageParam.FORWARD, [varint(BigInt(requestedForward))]);\n sub.setInitialForwardState(requestedForward);\n }\n if (filterBytes !== undefined) {\n parameters.set(MessageParam.SUBSCRIPTION_FILTER, [filterBytes]);\n // Store for draft-14 SUBSCRIBE_UPDATE replay\n sub.currentFilter = filterBytes;\n }\n if (fillBytes !== undefined) {\n parameters.set(FILL_PARAMETERS, [fillBytes]);\n if (sub.forwardState === ForwardState.ACTIVE) {\n this.registerFillRequest(requestId, sub, options.fill!, options.subscriptionFilter);\n }\n }\n\n const subscribeMsg: Subscribe = {\n type: 'SUBSCRIBE',\n requestId,\n trackNamespace: namespace,\n trackName: name,\n parameters,\n };\n\n return {\n requestId,\n actions: [this.sendControl(subscribeMsg)],\n };\n }\n\n /**\n * Initiate an outbound PUBLISH. We are the publisher: this\n * allocates a Request ID, records publisher-side subscription state with the\n * advertised Track Alias (PENDING until the peer's REQUEST_OK), and produces a\n * PUBLISH control message. Draft 14/16 use the control stream; draft 18 opens\n * a dedicated bidi request stream.\n *\n * `trackAlias` is a full uint64-capable bigint; the draft-18 encoder accepts the\n * whole range, while draft-14/16 encoders still range-check it.\n *\n * @param namespace Track namespace tuple\n * @param name Track name bytes\n * @param trackAlias The Track Alias the publisher advertises for this track\n * @param options Optional PUBLISH `parameters` and draft-18 `trackProperties`\n * (§2.5; non-empty Track Properties on draft-14/16 throw).\n * @see draft-ietf-moq-transport-14 §9.13\n * @see draft-ietf-moq-transport-16 §9.13\n * @see draft-ietf-moq-transport-18 §10.10\n */\n publish(\n namespace: Uint8Array[],\n name: Uint8Array,\n trackAlias: bigint,\n options: AuthorizationOptions & {\n parameters?: Parameters;\n trackProperties?: TrackProperties;\n /** draft-22: the accepted SUBSCRIBE_TRACKS that caused this PUBLISH. */\n subscribeTracksRequestId?: bigint;\n } = {},\n ): RequestResult {\n this.assertEstablishedOrDraining('publish');\n this.assertNotReservedNamespace(namespace, 'publish');\n\n if (this._state === SessionState.DRAINING) {\n throw new SessionDrainingError(\n 'Cannot PUBLISH after GOAWAY; session is DRAINING',\n this._newSessionUri ?? '',\n );\n }\n\n // Sender-side API validation must precede request-ID allocation and every\n // state mutation. The wire value is binary in every supported draft;\n // treating an arbitrary non-zero value as ACTIVE would emit a malformed\n // PUBLISH and leave a locally-authorized subscription behind if encoding\n // later rejected it.\n this.assertLocalForwardParameter(options.parameters, 'PUBLISH');\n if (options.subscribeTracksRequestId !== undefined) {\n const tracks = this.incomingTrackSubscriptions.get(options.subscribeTracksRequestId);\n if (!isDraft22(this._draftVersion) || tracks?.state !== 'active'\n || tracks.trackNamespacePrefix.length > namespace.length\n || !prefixesOverlap(tracks.trackNamespacePrefix, namespace)) {\n throw new SessionError('PUBLISH requires an active matching draft-22 SUBSCRIBE_TRACKS', 'INVALID_STATE');\n }\n // Inherit only when the caller identifies this as a resulting PUBLISH.\n // Independent publications retain publisher authority (§9.8 / §9.18.1).\n const parameters = new Map(options.parameters);\n // Delivery settings become the resulting subscription's initial policy\n // (22 sections 3.6.2 and 9.8). Authorization belongs to the sender and\n // MUST NOT be copied (9.20.2); filters not valid on PUBLISH stay local.\n for (const key of [MessageParam.GROUP_ORDER, MessageParam.SUBSCRIBER_PRIORITY,\n MessageParam.SUBSCRIPTION_FILTER, MessageParam.OBJECT_DELIVERY_TIMEOUT, MessageParam.SUBGROUP_DELIVERY_TIMEOUT]) {\n const values = tracks.parameters.get(key);\n if (values !== undefined) parameters.set(key, values);\n }\n parameters.set(MessageParam.FORWARD, [varint(BigInt(tracks.forward))]);\n options = { ...options, parameters };\n }\n this.assertPublisherAliasAvailable(trackAlias, namespace, name);\n if (isDraft22(this._draftVersion)) {\n const error = this.validateMessageParams(options.parameters ?? new Map(), 'PUBLISH');\n if (error) throw new SessionError(error.reason, 'INVALID_STATE');\n }\n\n const trackProperties = this.resolveTrackProperties(options.trackProperties, 'PUBLISH');\n const parameters = authorizationParameters(options, options.parameters);\n\n const requestId = this.requestIdAllocator.allocate();\n\n const sub = SubscriptionStateMachine.createAsPublisher(requestId, namespace, name);\n sub.setOutboundPublishAlias(trackAlias);\n // §5.1 (d18:1948): \"A publisher MUST save the Largest Location\n // communicated in ... PUBLISH\" — it is this subscription's Joining\n // Location, exactly as with SUBSCRIBE_OK.\n const communicated = this.extractLargestObjectParam(options.parameters);\n if (communicated) this.incomingJoiningLocations.set(requestId as bigint, communicated);\n if (isDraft22(this._draftVersion)) this.initializePublishPolicy22(sub, options.parameters ?? new Map(), trackProperties);\n // The PUBLISH's FORWARD parameter sets the subscription's initial Forward\n // State (§10.10) — the d18 joining-fetch gate consults it at establish.\n const initialForward = options.parameters?.get(MessageParam.FORWARD)?.[0];\n if (typeof initialForward === 'bigint') {\n sub.setInitialForwardState(initialForward === 0n ? ForwardState.PAUSED : ForwardState.ACTIVE);\n }\n this.outgoingPublishes.set(requestId as bigint, sub);\n\n const publishMsg: Publish = {\n type: 'PUBLISH',\n requestId,\n trackNamespace: namespace,\n trackName: name,\n trackAlias,\n parameters,\n trackProperties,\n };\n\n return {\n requestId,\n actions: [this.sendControl(publishMsg)],\n };\n }\n\n /** Get an outgoing PUBLISH (publisher-side) subscription by request ID. */\n getOutgoingPublish(requestId: bigint): SubscriptionStateMachine | undefined {\n return this.outgoingPublishes.get(requestId as bigint);\n }\n\n /**\n * Get a subscription by request ID.\n */\n getSubscription(requestId: bigint): SubscriptionStateMachine | undefined {\n return this.subscriptions.get(requestId as bigint);\n }\n\n /**\n * Cancel an established subscriber-side subscription.\n *\n * §2.4.2: \"When a subscriber detects a Malformed Track, it MUST\n * UNSUBSCRIBE any subscription [...] for that Track from that publisher.\"\n *\n * @param requestId The request ID of the subscription to unsubscribe\n * @returns draft-14/16: a `send_control` action with the UNSUBSCRIBE message;\n * draft-18: no actions (UNSUBSCRIBE was removed — the I/O layer tears down the\n * request stream instead).\n * @see draft-ietf-moq-transport-16 §2.4.2 (Malformed Track)\n * @see draft-ietf-moq-transport-16 §5.1 (Subscription lifecycle)\n */\n unsubscribe(requestId: bigint): SessionOutboundAction[] {\n const sub = this.subscriptions.get(requestId as bigint);\n if (!sub) {\n throw new SessionError(\n `Unknown subscription ${requestId} for unsubscribe`,\n 'INVALID_STATE',\n );\n }\n\n // §5.1: a subscriber may UNSUBSCRIBE from the Pending (Subscriber) OR\n // Established state. Reclaim the full state; the crossed peer SEQUENCE\n // (SUBSCRIBE_OK then PUBLISH_DONE / REQUEST_ERROR) is tolerated via the recorded\n // shadow and reclaimed deterministically (see recentlyCancelled).\n this.terminateLocalSubscriberRequest(sub);\n\n // draft-18 removed the UNSUBSCRIBE message; the subscriber cancels by tearing\n // down the request stream (RESET_STREAM + STOP_SENDING, §3.3.2) — the I/O layer\n // does that; emit NO control message. draft-14/16 send UNSUBSCRIBE.\n if (isRequestStreamDraft(this._draftVersion)) return [];\n return [this.sendControl({ type: 'UNSUBSCRIBE', requestId } as Unsubscribe)];\n }\n\n // ─── Request Update Operations ─────────────────────────────────────────\n\n /**\n * Roll back a REQUEST_UPDATE whose SEND failed before the peer received it. The\n * update allocated a Request ID and a {@link pendingUpdates} record (drafts\n * 16/18 defer the state change to REQUEST_OK), which must be dropped so a later\n * response cannot be mis-applied to an update that never went out — and so the\n * pending map does not leak on a transport write failure. No-op once resolved.\n * (draft-14 applies the change inline with no pending record; a draft-14 update\n * send failure is a control-stream error handled session-fatally.)\n */\n rollbackRequestUpdate(requestId: bigint): void {\n this.pendingUpdates.delete(requestId as bigint);\n this.fillRequests.delete(requestId as bigint);\n }\n\n private validateUpdateContext22(params: Parameters, requestType: string): SessionOutboundAction[] | undefined {\n if (!isDraft22(this._draftVersion)) return undefined;\n for (const key of params.keys()) {\n const scopes = VALID_PARAMS_FOR_MESSAGE_TYPE_22.get(key);\n if (!scopes) continue;\n const prefixUpdate = key === MessageParam.TRACK_NAMESPACE_PREFIX\n && (requestType === 'SUBSCRIBE_NAMESPACE' || requestType === 'SUBSCRIBE_TRACKS');\n const subscriptionOnly = key === FILL_PARAMETERS || key === MessageParam.SUBSCRIPTION_FILTER\n || (key >= 0x25n && key <= 0x28n);\n if (!prefixUpdate && (!scopes.has(requestType) || (requestType === 'FETCH' && subscriptionOnly))) {\n return this.closeWithError(SessionErrorCode.PROTOCOL_VIOLATION,\n `Parameter 0x${key.toString(16)} is out of scope for REQUEST_UPDATE on ${requestType} (9.20)`);\n }\n }\n return undefined;\n }\n\n /**\n * Send a REQUEST_UPDATE to modify an existing subscription.\n * The forward state is not updated locally until REQUEST_OK is received.\n * @see draft-ietf-moq-transport-16 §9.11\n */\n requestUpdate(\n existingRequestId: bigint,\n options: RequestUpdateOptions = {},\n ): RequestResult {\n this.assertEstablishedOrDraining('requestUpdate');\n const requestedForward = this.normalizeLocalForwardState(options.forward, 'REQUEST_UPDATE');\n // draft-22 §9.1.7: never more than the peer's MAX_REQUEST_UPDATES unanswered\n // updates on one request stream; each REQUEST_OK / REQUEST_ERROR restores one.\n if (isDraft22(this._draftVersion) && this._peerMaxRequestUpdates > 0n) {\n let outstanding = 0n;\n for (const pending of this.pendingUpdates.values()) {\n if (pending.existingRequestId === (existingRequestId as bigint)) outstanding += 1n;\n }\n if (outstanding >= this._peerMaxRequestUpdates) {\n throw new SessionError(\n `${outstanding} REQUEST_UPDATEs on request ${existingRequestId} are unanswered; the peer's MAX_REQUEST_UPDATES is ${this._peerMaxRequestUpdates}`,\n 'INVALID_STATE',\n );\n }\n }\n if (options.fill !== undefined && !isDraft22(this._draftVersion)) {\n throw new RangeError(`FILL_PARAMETERS needs draft 22; this session speaks draft ${this._draftVersion}`);\n }\n const parameters = authorizationParameters(options);\n\n // draft-18 §10.9.2: a prefix update targets an outbound SUBSCRIBE_NAMESPACE /\n // SUBSCRIBE_TRACKS request (tracked in separate maps, not `subscriptions`).\n if (this.namespaceSubscriptions.has(existingRequestId as bigint)\n || this.trackSubscriptions.has(existingRequestId as bigint)) {\n return this.requestUpdatePrefix(existingRequestId, options);\n }\n\n const sub = this.subscriptions.get(existingRequestId as bigint);\n if (!sub) {\n throw new SessionError(\n `Unknown subscription ${existingRequestId} for REQUEST_UPDATE`,\n 'INVALID_STATE',\n );\n }\n\n // §10.9.2: a Track Namespace Prefix update is only valid for a\n // SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS, never a normal subscription — reject\n // it loudly rather than silently dropping the option.\n if (options.trackNamespacePrefix !== undefined) {\n throw new SessionError(\n `trackNamespacePrefix is only valid for a SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS update, not subscription ${existingRequestId}`,\n 'INVALID_STATE',\n );\n }\n\n // draft-18 §10.12.2 contemplates REQUEST_UPDATE racing establishment\n // (\"process any pending REQUEST_UPDATE messages ... before evaluating\"),\n // so a d18 subscriber may update a still-PENDING subscription.\n const updatableWhilePending = isRequestStreamDraft(this._draftVersion) && sub.state === 'pending';\n if (sub.state !== 'established' && !updatableWhilePending) {\n throw new SessionError(\n `Cannot update subscription in state ${sub.state}; expected established`,\n 'INVALID_STATE',\n );\n }\n\n const filterBytes = options.subscriptionFilter === undefined ? undefined\n : encodeSubscriptionFilter(options.subscriptionFilter, this._draftVersion);\n const fillBytes = options.fill === undefined ? undefined\n : encodeFillParameters(options.fill.filter, options.fill.groupOrder);\n if (options.subgroupDeliveryTimeout !== undefined && !isRequestStreamDraft(this._draftVersion)) {\n throw new SessionError('subgroupDeliveryTimeout (SUBGROUP_DELIVERY_TIMEOUT, 0x06) is draft-18 only', 'INVALID_STATE');\n }\n const requestId = this.requestIdAllocator.allocate();\n\n // Build parameters\n // FORWARD parameter\n if (requestedForward !== undefined) {\n parameters.set(MessageParam.FORWARD, [varint(BigInt(requestedForward))]);\n } else if (this._draftVersion === 14) {\n // Draft-14 §9.10: Forward is a mandatory inline field.\n // Replay current value to avoid unintentional state change.\n const currentForward = varint(BigInt(sub.forwardState));\n parameters.set(MessageParam.FORWARD, [currentForward]);\n }\n\n // SUBSCRIBER_PRIORITY parameter\n if (options.subscriberPriority !== undefined) {\n parameters.set(MessageParam.SUBSCRIBER_PRIORITY, [options.subscriberPriority]);\n sub.currentPriority = options.subscriberPriority;\n } else if (this._draftVersion === 14 && sub.currentPriority !== undefined) {\n // Draft-14 §9.10: Subscriber Priority is a mandatory inline field.\n // Replay current value to avoid resetting to codec default (128).\n parameters.set(MessageParam.SUBSCRIBER_PRIORITY, [sub.currentPriority]);\n }\n\n // §9.2.2.5: SUBSCRIPTION_FILTER MAY appear in REQUEST_UPDATE\n if (filterBytes !== undefined) {\n parameters.set(MessageParam.SUBSCRIPTION_FILTER, [filterBytes]);\n // Update stored filter for future draft-14 replays\n if (!isDraft22(this._draftVersion)) sub.currentFilter = filterBytes;\n } else if (this._draftVersion === 14 && sub.currentFilter) {\n // Draft-14 §9.10: Start Location and End Group are mandatory inline fields.\n // If no new filter specified, replay the current filter to avoid widening.\n parameters.set(MessageParam.SUBSCRIPTION_FILTER, [sub.currentFilter]);\n }\n\n // §8: OBJECT/SUBGROUP_DELIVERY_TIMEOUT MAY appear in REQUEST_UPDATE. STAGE the new\n // values on the pending record (drafts 16/18 apply on REQUEST_OK; draft-14 inline\n // below) so a rejected update / write rollback cannot commit them prematurely. An\n // omitted parameter leaves the current value unchanged.\n if (options.objectDeliveryTimeout !== undefined) {\n parameters.set(MessageParam.OBJECT_DELIVERY_TIMEOUT, [options.objectDeliveryTimeout]);\n }\n if (options.subgroupDeliveryTimeout !== undefined) {\n parameters.set(MessageParam.SUBGROUP_DELIVERY_TIMEOUT, [options.subgroupDeliveryTimeout]);\n }\n if (fillBytes !== undefined) {\n parameters.set(FILL_PARAMETERS, [fillBytes]);\n this.registerFillRequest(requestId, sub, options.fill!, options.subscriptionFilter);\n }\n\n if (this._draftVersion === 14) {\n // Draft-14 §9.10: \"There is no control message in response to a\n // SUBSCRIBE_UPDATE.\" Apply state changes immediately.\n if (requestedForward !== undefined) {\n sub.updateForwardState(requestedForward);\n }\n if (options.objectDeliveryTimeout !== undefined) {\n sub.requestedDeliveryTimeoutMs = Number(options.objectDeliveryTimeout);\n }\n } else {\n // Draft-16/18: track pending update — ALL state (forward + timeouts) is applied\n // on REQUEST_OK, never at send (a rejected update must not commit anything).\n const pending: {\n existingRequestId: bigint; forward?: ForwardStateValue;\n objectDeliveryTimeoutMs?: number; subgroupDeliveryTimeoutMs?: number;\n subscriptionFilter?: Uint8Array;\n } = { existingRequestId: existingRequestId as bigint };\n if (requestedForward !== undefined) {\n pending.forward = requestedForward;\n }\n if (options.objectDeliveryTimeout !== undefined) pending.objectDeliveryTimeoutMs = Number(options.objectDeliveryTimeout);\n if (options.subgroupDeliveryTimeout !== undefined) pending.subgroupDeliveryTimeoutMs = Number(options.subgroupDeliveryTimeout);\n if (isDraft22(this._draftVersion) && options.subscriptionFilter !== undefined) {\n pending.subscriptionFilter = parameters.get(MessageParam.SUBSCRIPTION_FILTER)![0] as Uint8Array;\n }\n this.pendingUpdates.set(requestId as bigint, pending);\n }\n\n const updateMsg: RequestUpdate = {\n type: 'REQUEST_UPDATE',\n requestId,\n existingRequestId,\n parameters,\n };\n\n return {\n requestId,\n actions: [this.sendControl(updateMsg)],\n };\n }\n\n /**\n * draft-18 §10.9.2: send a REQUEST_UPDATE that changes the Track Namespace\n * Prefix of an outbound SUBSCRIBE_NAMESPACE or SUBSCRIBE_TRACKS. The update\n * rides the request's existing (continuing) bidi stream; the new prefix is\n * applied locally only when the matching REQUEST_OK arrives.\n */\n private requestUpdatePrefix(\n existingRequestId: bigint,\n options: RequestUpdateOptions,\n ): RequestResult {\n if (!isRequestStreamDraft(this._draftVersion)) {\n throw new SessionError(\n `Track Namespace Prefix REQUEST_UPDATE requires draft-18 (current draft-${this._draftVersion})`,\n 'INVALID_STATE',\n );\n }\n const nsSub = this.namespaceSubscriptions.get(existingRequestId as bigint);\n const trackSub = this.trackSubscriptions.get(existingRequestId as bigint);\n const target: 'namespace' | 'tracks' = nsSub ? 'namespace' : 'tracks';\n\n if (nsSub && !nsSub.isActive) {\n throw new SessionError(`Cannot update SUBSCRIBE_NAMESPACE ${existingRequestId}: not active`, 'INVALID_STATE');\n }\n if (trackSub && trackSub.state !== 'active') {\n throw new SessionError(`Cannot update SUBSCRIBE_TRACKS ${existingRequestId}: not active`, 'INVALID_STATE');\n }\n\n const prefix = options.trackNamespacePrefix;\n const forward = this.normalizeLocalForwardState(options.forward, 'REQUEST_UPDATE');\n const supportsForward = isDraft22(this._draftVersion) && trackSub !== undefined;\n if (!prefix && !(supportsForward && forward !== undefined)) {\n throw new SessionError(\n `REQUEST_UPDATE for a ${target === 'namespace' ? 'SUBSCRIBE_NAMESPACE' : 'SUBSCRIBE_TRACKS'} requires trackNamespacePrefix`,\n 'INVALID_STATE',\n );\n }\n if (forward !== undefined && !supportsForward) {\n throw new SessionError('FORWARD prefix updates require draft-22 SUBSCRIBE_TRACKS', 'INVALID_STATE');\n }\n try {\n if (prefix) validateTrackNamespacePrefix(prefix);\n } catch (e) {\n throw new SessionError(e instanceof Error ? e.message : 'Malformed Track Namespace Prefix', 'PROTOCOL_VIOLATION');\n }\n\n const parameters = authorizationParameters(options);\n if (prefix) parameters.set(MessageParam.TRACK_NAMESPACE_PREFIX, [prefix]);\n const requestId = this.requestIdAllocator.allocate();\n if (forward !== undefined) parameters.set(MessageParam.FORWARD, [varint(BigInt(forward))]);\n this.pendingUpdates.set(requestId as bigint, {\n existingRequestId: existingRequestId as bigint,\n ...(prefix === undefined ? {} : { namespacePrefix: prefix }),\n prefixTarget: target,\n ...(forward === undefined ? {} : { forward }),\n });\n const updateMsg: RequestUpdate = { type: 'REQUEST_UPDATE', requestId, existingRequestId, parameters };\n return { requestId, actions: [this.sendControl(updateMsg)] };\n }\n\n // ─── Fetch Operations ─────────────────────────────────────────────────\n\n /**\n * Create a new fetch request.\n */\n fetch(\n namespace: Uint8Array[],\n name: Uint8Array,\n options: FetchOptions,\n ): RequestResult {\n this.assertEstablishedOrDraining('fetch');\n const parameters = authorizationParameters(options);\n\n if (this._state === SessionState.DRAINING) {\n throw new SessionDrainingError(\n 'Cannot create new fetches after GOAWAY; session is DRAINING',\n this._newSessionUri ?? '',\n );\n }\n\n const requestId = this.requestIdAllocator.allocate();\n\n const fetchSm = FetchStateMachine.createAsFetcher(\n requestId,\n options.startGroup,\n options.startObject,\n options.endGroup,\n options.endObject,\n );\n this.fetches.set(requestId as bigint, fetchSm);\n\n // Build StandaloneFetch with proper Location fields per §9.16.1\n const startLocation = {\n group: options.startGroup,\n object: options.startObject,\n };\n const endLocation = {\n group: options.endGroup ?? 0n,\n object: options.endObject ?? 0n,\n };\n\n // §9.16: \"End Location MUST specify the same or a larger Location\n // than Start Location for Standalone and Absolute Joining Fetches.\"\n if (\n endLocation.group < startLocation.group ||\n (endLocation.group === startLocation.group &&\n endLocation.object < startLocation.object)\n ) {\n throw new SessionError(\n `FETCH endLocation (${endLocation.group},${endLocation.object}) < startLocation (${startLocation.group},${startLocation.object}) — §9.16`,\n 'INVALID_RANGE',\n );\n }\n\n const standaloneFetch: StandaloneFetch = {\n fetchType: 0x1,\n trackNamespace: namespace,\n trackName: name,\n startLocation,\n endLocation,\n };\n\n // §10.2: a requested Group Order travels as the GROUP_ORDER (0x22) parameter\n // — Ascending = 0x1, Descending = 0x2. Omitted ⇒ Ascending on decode.\n if (options.groupOrder !== undefined) {\n parameters.set(MessageParam.GROUP_ORDER, [varint(options.groupOrder === 'descending' ? 2n : 1n)]);\n }\n\n const fetchMsg: Fetch = {\n type: 'FETCH',\n requestId,\n fetch: standaloneFetch,\n parameters,\n };\n\n return {\n requestId,\n actions: [this.sendControl(fetchMsg)],\n };\n }\n\n /**\n * Create a Joining Fetch (§9.16.2 / draft-18 §10.12.2) referencing one of\n * OUR subscriptions in PENDING or ESTABLISHED state.\n *\n * The referenced subscription SHOULD use the Largest Object filter: on\n * draft-14/16 any other filter is a session-fatal PROTOCOL_VIOLATION at the\n * publisher (§9.16.2); draft-18 gates on Forward State instead (§10.12.2).\n * Joining a PENDING subscription is legal — draft-18 additionally requires\n * the publisher to buffer the fetch until the subscription resolves.\n *\n * @throws {SessionError} INVALID_STATE if `joiningRequestId` is not one of\n * our subscriptions in PENDING/ESTABLISHED — a reference implementation\n * never emits a request the peer is required to reject\n * (INVALID_JOINING_REQUEST_ID).\n */\n joiningFetch(options: JoiningFetchOptions): RequestResult {\n this.assertEstablishedOrDraining('joiningFetch');\n const parameters = authorizationParameters(options);\n\n if (this._state === SessionState.DRAINING) {\n throw new SessionDrainingError(\n 'Cannot create new fetches after GOAWAY; session is DRAINING',\n this._newSessionUri ?? '',\n );\n }\n\n // A subscription WE consume may be our outbound SUBSCRIBE or an inbound\n // PUBLISH we accepted (we are the subscriber for both) — a join may\n // reference either (§9.16.2 / d18 §10.12.2). Peer SUBSCRIBEs we SERVE are\n // not joinable by us. The eligibility matrix mirrors the publisher side:\n // draft-14 requires the subscription ESTABLISHED; 16/18 additionally\n // allow a PENDING outbound SUBSCRIBE (never a pending inbound PUBLISH —\n // it is invalid until our PUBLISH_OK establishes it).\n const inbound = this.incomingSubscriptions.get(options.joiningRequestId as bigint);\n const outbound = this.subscriptions.get(options.joiningRequestId as bigint);\n const sub = outbound ?? (inbound?.isPublishInitiated ? inbound : undefined);\n const joinable = sub !== undefined && (\n sub.state === SubscriptionState.ESTABLISHED\n || (this._draftVersion !== 14 && sub.state === SubscriptionState.PENDING && sub === outbound)\n );\n if (!joinable) {\n throw new SessionError(\n `joiningFetch: request ${options.joiningRequestId} is not a joinable subscription (draft-14: established only; 16/18: established or pending SUBSCRIBE) — §9.16.2`,\n 'INVALID_STATE',\n );\n }\n\n const requestId = this.requestIdAllocator.allocate();\n const fetchType = options.joiningFetchType === 'absolute' ? 0x3 as const : 0x2 as const;\n\n const fetchSm = FetchStateMachine.createAsJoiningFetcher(requestId, {\n fetchType,\n joiningRequestId: options.joiningRequestId,\n joiningStart: options.joiningStart,\n });\n this.fetches.set(requestId as bigint, fetchSm);\n\n // No width narrowing here: the field is vi64 on draft-18 (a peer-allocated\n // PUBLISH Request ID may exceed the QUIC-varint range), and the selected\n // draft codec enforces its own width on encode (d14/16 reject out-of-range).\n const joiningFetch: JoiningFetch = {\n fetchType,\n joiningRequestId: options.joiningRequestId,\n joiningStart: options.joiningStart,\n };\n\n // §10.2: a requested Group Order travels as the GROUP_ORDER (0x22) parameter.\n if (options.groupOrder !== undefined) {\n parameters.set(MessageParam.GROUP_ORDER, [varint(options.groupOrder === 'descending' ? 2n : 1n)]);\n }\n\n const fetchMsg: Fetch = {\n type: 'FETCH',\n requestId,\n fetch: joiningFetch,\n parameters,\n };\n\n return {\n requestId,\n actions: [this.sendControl(fetchMsg)],\n };\n }\n\n /**\n * Resolve an incoming Joining Fetch against the associated subscription's\n * SAVED Joining Location (§9.16.2.1; draft-18 §5.1) — the Largest Location\n * this publisher communicated in SUBSCRIBE_OK / PUBLISH / a Forward-0→1\n * REQUEST_UPDATE_OK — back-filling the publisher-side fetch state\n * machine's range and returning it. There is no app-supplied anchor and no\n * fallback: a subscription with no saved snapshot cannot be joined, and\n * the method throws so the caller rejects with REQUEST_ERROR INVALID_RANGE.\n *\n * @returns the standalone-equivalent range in the FETCH wire convention\n * (`endLocation.object` is one-past the last delivered object), computed\n * from the saved anchor via {@link resolveJoiningFetchRange}.\n * @throws {SessionError} INVALID_STATE for an unknown request, a\n * standalone fetch, or a subscription with no saved Joining Location\n * (caller maps the last to REQUEST_ERROR INVALID_RANGE);\n * {@link RangeError} for an Absolute Joining Fetch whose start exceeds\n * the largest group (same INVALID_RANGE mapping per §9.16.3).\n */\n resolveIncomingJoiningFetch(\n requestId: bigint,\n ): { startLocation: { group: bigint; object: bigint }; endLocation: { group: bigint; object: bigint } } {\n const fetchSm = this.incomingFetches.get(requestId as bigint);\n if (!fetchSm || !fetchSm.isJoining) {\n throw new SessionError(\n `resolveIncomingJoiningFetch: request ${requestId} is not an incoming joining fetch`,\n 'INVALID_STATE',\n );\n }\n const joining = fetchSm.joining!;\n // §5.1 (d16:1381 / d18:1948): the SAVED Joining Location — the Largest\n // Location this publisher communicated in SUBSCRIBE_OK (or a d18 Forward\n // 0→1 REQUEST_UPDATE_OK) — is the ONLY valid join anchor. There is\n // deliberately no caller-supplied fallback: a later head can gap or\n // overlap the subscription's delivery boundary, and a subscription whose\n // SUBSCRIBE_OK communicated no Largest has no Joining Location at all —\n // the compliant response to its join is REQUEST_ERROR INVALID_RANGE\n // (which this throw lets the caller map to).\n const anchor = this.incomingJoiningLocations.get(joining.joiningRequestId as bigint);\n if (!anchor) {\n throw new SessionError(\n `resolveIncomingJoiningFetch: subscription ${joining.joiningRequestId} has no saved Joining Location (no LARGEST_OBJECT was communicated) — reject the join with INVALID_RANGE`,\n 'INVALID_STATE',\n );\n }\n const range = resolveJoiningFetchRange(\n { fetchType: joining.fetchType, joiningStart: joining.joiningStart },\n anchor,\n );\n fetchSm.setResolvedRange(\n range.startLocation.group,\n range.startLocation.object,\n range.endLocation.group,\n range.endLocation.object,\n );\n return range;\n }\n\n /**\n * Get a fetch by request ID.\n */\n getFetch(requestId: bigint): FetchStateMachine | undefined {\n return this.fetches.get(requestId as bigint);\n }\n\n /**\n * Cancel an outgoing fetch request, transitioning the fetch to COMPLETED.\n *\n * draft-14/16: returns a FETCH_CANCEL to send on the control stream.\n * §9.18: \"A subscriber sends a FETCH_CANCEL message to a publisher to\n * indicate it is no longer interested in receiving objects for the fetch\n * identified by the 'Request ID'.\"\n * §5.2: \"A subscriber keeps FETCH state until it sends FETCH_CANCEL,\n * receives REQUEST_ERROR, or receives a FIN or RESET_STREAM for the FETCH\n * data stream.\"\n *\n * draft-18: FETCH_CANCEL was removed (§3.3.2); this marks the fetch completed\n * and returns NO actions — the I/O layer cancels the request + data streams\n * (STOP_SENDING / RESET_STREAM).\n *\n * @see draft-ietf-moq-transport-16 §5.2, §9.18; draft-ietf-moq-transport-18 §3.3.2\n */\n fetchCancel(requestId: bigint): SessionOutboundAction[] {\n const fetch = this.fetches.get(requestId as bigint);\n if (!fetch) {\n throw new SessionError(\n `Unknown fetch ${requestId} for FETCH_CANCEL`,\n 'INVALID_STATE',\n );\n }\n\n // Capture the phase BEFORE sendFetchCancel transitions the SM.\n const established = fetch.isTransferring;\n fetch.sendFetchCancel();\n\n // Drop the fetch from tracking. Record a crossed-message shadow ONLY for a\n // Pending FETCH cancellation: a transferring FETCH has received its\n // FETCH_OK, so no crossed FETCH_OK is possible and — §5.2 — no REQUEST_ERROR is\n // legal after the OK, so an established fetch needs no shadow at all. The pending\n // shadow is ONE-SHOT (the first crossed FETCH_OK / REQUEST_ERROR reclaims it),\n // sparing the now-COMPLETED state machine an assertion-close on a crossed message.\n this.fetches.delete(requestId as bigint);\n if (!established) this.recordCancellation(requestId, 'fetch', false);\n\n if (isRequestStreamDraft(this._draftVersion)) {\n // draft-18 removed the FETCH_CANCEL control message (§3.3.2): cancellation\n // is STOP_SENDING / RESET_STREAM on the request + data streams, performed by\n // the I/O layer. No control message is emitted.\n return [];\n }\n\n const fetchCancelMsg: FetchCancel = {\n type: 'FETCH_CANCEL',\n requestId,\n };\n\n return [this.sendControl(fetchCancelMsg)];\n }\n\n /**\n * §5.2: the fetcher keeps FETCH state until it sends FETCH_CANCEL, receives\n * REQUEST_ERROR, or the FETCH data stream FINs/resets. The I/O layer calls this on\n * that data-stream close to RECLAIM the completed outgoing fetch (bounded — the\n * fetch is done, no crossed control response is expected after a clean end).\n * No-op if the fetch is already gone.\n */\n handleFetchStreamFinished(requestId: bigint): void {\n const fetch = this.fetches.get(requestId as bigint);\n if (!fetch) return;\n // §10.13: the data stream ended. Reclaim only once the FETCH_OK response has\n // ALSO been seen; if it has NOT arrived yet, KEEP the fetch so a later FETCH_OK\n // is matched (not treated as an unknown-request §9.1 close). handleFetchOk\n // reclaims when it lands after the data.\n fetch.markDataFinished();\n if (fetch.isFetcherComplete) this.fetches.delete(requestId as bigint);\n }\n\n /**\n * PUBLISHER side: the response stream for an incoming FETCH has closed (§10.13).\n * §10.13 lets the data stream close BEFORE FETCH_OK is sent, so RECLAIM only once\n * BOTH are done; otherwise keep the fetch so a later acceptFetch still finds it.\n * No-op if already gone.\n */\n reclaimServedFetch(requestId: bigint): void {\n const fetch = this.incomingFetches.get(requestId as bigint);\n if (!fetch) return;\n fetch.markDataFinished();\n if (fetch.isPublisherComplete) this.incomingFetches.delete(requestId as bigint);\n }\n\n // ─── Track Alias Operations ───────────────────────────────────────────\n\n /**\n * Get track info by alias.\n */\n getTrackByAlias(alias: bigint) {\n return this.trackAliases.getByAlias(alias);\n }\n\n /** Live subscriber-side requests sharing one peer-assigned Track Alias. */\n getSubscriberRequestsForAlias(alias: bigint): SubscriptionStateMachine[] {\n const result: SubscriptionStateMachine[] = [];\n for (const id of this.trackAliases.owners(alias)) {\n const sub = this.subscriptions.get(id) ?? this.incomingSubscriptions.get(id);\n if (sub && !sub.isTerminated) result.push(sub);\n }\n return result;\n }\n\n // ─── Namespace Discovery Operations ──────────────────────────────────\n\n /**\n * Create a new namespace subscription.\n * Returns an open_namespace_stream action — the adapter should open a bidi\n * stream, send the SUBSCRIBE_NAMESPACE message on it, and associate the\n * stream with the requestId for routing future messages.\n * @see draft-ietf-moq-transport-16 §6.1, §9.25\n */\n subscribeNamespace(\n namespacePrefix: Uint8Array[],\n subscribeOptions: Varint = varint(0n),\n options: AuthorizationOptions = {},\n ): RequestResult {\n this.assertEstablishedOrDraining('subscribeNamespace');\n const parameters = authorizationParameters(options);\n\n if (this._state === SessionState.DRAINING) {\n throw new SessionDrainingError(\n 'Cannot create new namespace subscriptions after GOAWAY; session is DRAINING',\n this._newSessionUri ?? '',\n );\n }\n\n const requestId = this.requestIdAllocator.allocate();\n\n const nsSm = NamespaceStateMachine.createAsSubscriber(requestId, namespacePrefix);\n this.namespaceSubscriptions.set(requestId as bigint, nsSm);\n\n const subscribeMsg: SubscribeNamespace = {\n type: 'SUBSCRIBE_NAMESPACE',\n requestId,\n trackNamespacePrefix: namespacePrefix,\n ...(this._draftVersion === 14 ? {} : { subscribeOptions }),\n parameters,\n };\n\n if (this._draftVersion === 14) {\n // Draft-14 §9.28: SUBSCRIBE_NAMESPACE is sent on the control stream\n return {\n requestId,\n actions: [this.sendControl(subscribeMsg)],\n };\n }\n\n // Draft-16 §6.1: SUBSCRIBE_NAMESPACE opens a bidi stream\n const action: OpenNamespaceStreamAction = {\n type: 'open_namespace_stream',\n requestId,\n message: subscribeMsg,\n };\n\n return {\n requestId,\n actions: [action],\n };\n }\n\n // ─── SUBSCRIBE_TRACKS Operations (draft-18 §10.19) ───────────────────\n\n /**\n * Create a SUBSCRIBE_TRACKS request (draft-18 §10.19): ask a publisher for\n * PUBLISH messages for all tracks within matching namespaces. Like\n * SUBSCRIBE_NAMESPACE, it travels on a CONTINUING bidi request stream; the\n * I/O layer opens the stream and routes the first REQUEST_OK / REQUEST_ERROR\n * plus follow-up PUBLISH_BLOCKED messages.\n */\n subscribeTracks(namespacePrefix: Uint8Array[], options: AuthorizationOptions = {}): RequestResult {\n this.assertEstablishedOrDraining('subscribeTracks');\n if (this._state === SessionState.DRAINING) {\n throw new SessionDrainingError(\n 'Cannot create new track subscriptions after GOAWAY; session is DRAINING',\n this._newSessionUri ?? '',\n );\n }\n if (!isRequestStreamDraft(this._draftVersion)) {\n throw new SessionError('SUBSCRIBE_TRACKS is a draft-18 message', 'INVALID_STATE');\n }\n\n const parameters = authorizationParameters(options);\n const requestId = this.requestIdAllocator.allocate();\n this.trackSubscriptions.set(requestId as bigint, {\n trackNamespacePrefix: namespacePrefix,\n state: 'pending',\n blockedTracks: [],\n forward: ForwardState.ACTIVE,\n });\n\n const msg: SubscribeTracks = {\n type: 'SUBSCRIBE_TRACKS',\n requestId,\n trackNamespacePrefix: namespacePrefix,\n parameters,\n };\n return { requestId, actions: [this.sendControl(msg)] };\n }\n\n /** Get a SUBSCRIBE_TRACKS request's local state by Request ID. */\n getTrackSubscription(requestId: bigint): TrackSubscriptionState | undefined {\n return this.trackSubscriptions.get(requestId as bigint);\n }\n\n /**\n * Handle a message received on a SUBSCRIBE_TRACKS response stream AFTER the\n * first REQUEST_OK — i.e. PUBLISH_BLOCKED (§10.20). The first REQUEST_OK /\n * REQUEST_ERROR is routed through the normal stamped pipeline, not here.\n */\n handleSubscribeTracksStreamMessage(requestId: bigint, msg: ControlMessage): SessionOutboundAction[] {\n const ts = this.trackSubscriptions.get(requestId as bigint);\n if (!ts) {\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${requestId} for SUBSCRIBE_TRACKS stream message`,\n );\n }\n if (msg.type === 'PUBLISH_BLOCKED') {\n if (ts.state !== 'active') {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n 'PUBLISH_BLOCKED received before SUBSCRIBE_TRACKS was accepted',\n );\n }\n const pb = msg as PublishBlocked;\n ts.blockedTracks.push({ trackNamespaceSuffix: pb.trackNamespaceSuffix, trackName: pb.trackName });\n return [];\n }\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `Unexpected ${msg.type} on a SUBSCRIBE_TRACKS response stream`,\n );\n }\n\n /** Handle FIN / reset on a SUBSCRIBE_TRACKS response stream — terminate it. */\n handleSubscribeTracksStreamClosed(requestId: bigint): SessionOutboundAction[] {\n const ts = this.trackSubscriptions.get(requestId as bigint);\n if (!ts) return [];\n ts.state = 'terminated';\n this.trackSubscriptions.delete(requestId as bigint);\n return [];\n }\n\n // ─── PUBLISH_NAMESPACE Operations (§6.2) ──────────────────────────────\n\n /**\n * Advertise a namespace that this endpoint can publish on.\n *\n * Sends PUBLISH_NAMESPACE on the control stream. The peer responds\n * with PUBLISH_NAMESPACE_OK or PUBLISH_NAMESPACE_ERROR.\n *\n * @param namespace The namespace to advertise\n * @returns The request ID and actions to execute\n * @see draft-ietf-moq-transport-16 §6.2\n */\n publishNamespace(\n namespace: Uint8Array[],\n options: AuthorizationOptions = {},\n ): RequestResult {\n this.assertEstablishedOrDraining('publishNamespace');\n this.assertNotReservedNamespace(namespace, 'publishNamespace');\n const parameters = authorizationParameters(options);\n\n if (this._state === SessionState.DRAINING) {\n throw new SessionDrainingError(\n 'Cannot send PUBLISH_NAMESPACE after GOAWAY; session is DRAINING',\n this._newSessionUri ?? '',\n );\n }\n\n const requestId = this.requestIdAllocator.allocate();\n this.publishedNamespaces.set(requestId as bigint, {\n namespace,\n state: 'pending',\n });\n\n const publishNsMsg: PublishNamespace = {\n type: 'PUBLISH_NAMESPACE',\n requestId,\n trackNamespace: namespace,\n parameters,\n };\n\n return {\n requestId,\n actions: [this.sendControl(publishNsMsg)],\n };\n }\n\n /**\n * Withdraw an accepted namespace — stops serving new subscriptions.\n *\n * draft-14/16: emits PUBLISH_NAMESPACE_DONE on the control stream.\n * draft-18: PUBLISH_NAMESPACE_DONE was removed; withdrawal is a cancellation of\n * the PUBLISH_NAMESPACE request stream (§3.3.2). This method only terminates\n * local state and returns NO send_control action — the I/O layer cancels the\n * request stream.\n *\n * @param requestId The request ID from publishNamespace()\n * @throws If requestId is unknown or namespace is not yet accepted\n * @see draft-ietf-moq-transport-16 §9.22, draft-ietf-moq-transport-18 §3.3.2\n */\n publishNamespaceDone(requestId: bigint): SessionOutboundAction[] {\n this.assertEstablishedOrDraining('publishNamespaceDone');\n\n const entry = this.publishedNamespaces.get(requestId as bigint);\n if (!entry) {\n throw new SessionError(\n `Unknown request ID ${requestId} for publishNamespaceDone`,\n 'INVALID_STATE',\n );\n }\n if (entry.state !== 'active') {\n throw new SessionError(\n `Cannot withdraw pending namespace (requestId=${requestId})`,\n 'INVALID_STATE',\n );\n }\n\n this.publishedNamespaces.delete(requestId as bigint);\n\n // draft-18 §3.3.2: no PUBLISH_NAMESPACE_DONE on the wire — withdrawal is a\n // request-stream cancellation handled by the I/O layer. Terminate state only.\n if (isRequestStreamDraft(this._draftVersion)) {\n return [];\n }\n\n // v16 §9.22: PUBLISH_NAMESPACE_DONE { RequestID }\n // v14 §9.26: PUBLISH_NAMESPACE_DONE { TrackNamespace (tuple) }\n const msg: PublishNamespaceDone = this._draftVersion === 14\n ? { type: 'PUBLISH_NAMESPACE_DONE', trackNamespace: entry.namespace }\n : { type: 'PUBLISH_NAMESPACE_DONE', requestId };\n\n return [this.sendControl(msg)];\n }\n\n // ─── TRACK_STATUS Operations (§9.19) ──────────────────────────────────\n\n /**\n * Send a TRACK_STATUS request to query the current status of a track.\n *\n * §9.19: \"A potential subscriber sends a TRACK_STATUS message on the control\n * stream to obtain information about the current status of a given track.\"\n *\n * Does NOT create subscription state. Response arrives via REQUEST_OK or REQUEST_ERROR.\n * The publisher does not send PUBLISH_DONE, and the subscriber cannot send\n * REQUEST_UPDATE or UNSUBSCRIBE for this request.\n *\n * @see draft-ietf-moq-transport-16 §9.19\n */\n trackStatus(\n namespace: Uint8Array[],\n name: Uint8Array,\n options: AuthorizationOptions = {},\n ): RequestResult {\n this.assertEstablishedOrDraining('trackStatus');\n const parameters = authorizationParameters(options);\n\n if (this._state === SessionState.DRAINING) {\n throw new SessionDrainingError(\n 'Cannot send TRACK_STATUS after GOAWAY; session is DRAINING',\n this._newSessionUri ?? '',\n );\n }\n\n const requestId = this.requestIdAllocator.allocate();\n\n // §9.19: No subscription state created\n this.pendingTrackStatuses.set(requestId as bigint, { namespace, name });\n\n const trackStatusMsg: TrackStatus = {\n type: 'TRACK_STATUS',\n requestId,\n trackNamespace: namespace,\n trackName: name,\n parameters,\n };\n\n return {\n requestId,\n actions: [this.sendControl(trackStatusMsg)],\n };\n }\n\n /**\n * Resolve Track Properties (§2.5) for an outbound message. The `trackProperties`\n * send API is draft-18-only: draft-14/16 carry the legacy \"Track Extensions\" but\n * this API does not target them, so non-empty Track Properties on draft-14/16\n * throw rather than being silently encoded. Returns the map to attach (possibly\n * empty).\n */\n private resolveTrackProperties(trackProperties: TrackProperties | undefined, context: string): TrackProperties {\n const props = trackProperties ?? new Map();\n if (!isRequestStreamDraft(this._draftVersion) && props.size > 0) {\n throw new SessionError(\n `Track Properties on ${context} require draft-18 (current draft-${this._draftVersion})`,\n 'INVALID_STATE',\n );\n }\n return props;\n }\n\n /**\n * Accept an incoming TRACK_STATUS request (publisher-side, TRACK_STATUS_OK).\n *\n * §9.19: \"If successful, the publisher responds with a REQUEST_OK message\n * with the same parameters it would have set in a SUBSCRIBE_OK.\" draft-18 also\n * allows Track Properties (§2.5) on the TRACK_STATUS_OK.\n *\n * Backwards-compatible: the second argument may be the legacy `Parameters` map,\n * or a {@link TrackStatusAcceptOptions} carrying `parameters` and/or\n * `trackProperties`. Non-empty Track Properties on draft-14/16 throw (no such\n * field exists on the wire there).\n *\n * @see draft-ietf-moq-transport-16 §9.19, draft-ietf-moq-transport-18 §10.14\n */\n acceptTrackStatus(\n requestId: bigint,\n paramsOrOptions: Parameters | TrackStatusAcceptOptions = new Map(),\n ): SessionOutboundAction[] {\n const entry = this.incomingTrackStatuses.get(requestId as bigint);\n if (!entry) {\n throw new SessionError(\n `Unknown incoming TRACK_STATUS ${requestId}`,\n 'INVALID_STATE',\n );\n }\n\n // The legacy second arg is a Parameters Map; otherwise it is an options object.\n const opts: TrackStatusAcceptOptions =\n paramsOrOptions instanceof Map ? { parameters: paramsOrOptions } : paramsOrOptions;\n const parameters = opts.parameters ?? new Map();\n const trackProperties = this.resolveTrackProperties(opts.trackProperties, 'TRACK_STATUS_OK');\n\n this.incomingTrackStatuses.delete(requestId as bigint);\n\n const requestOk: RequestOk = {\n type: 'REQUEST_OK',\n requestId,\n parameters,\n // Carry Track Properties only when present; the codec encodes an empty\n // block as zero bytes regardless, but keeping it absent is cleaner for\n // draft-14/16 (whose codec has no Track Properties field).\n ...(trackProperties.size > 0 ? { trackProperties } : {}),\n };\n\n return [this.sendControl(requestOk)];\n }\n\n /**\n * Reject an incoming TRACK_STATUS request (publisher-side).\n *\n * §9.19: \"A publisher responds to a failed TRACK_STATUS with an\n * appropriate REQUEST_ERROR message.\"\n *\n * @see draft-ietf-moq-transport-16 §9.19\n */\n rejectTrackStatus(requestId: bigint, errorCode: bigint, errorReason: string): SessionOutboundAction[] {\n const entry = this.incomingTrackStatuses.get(requestId as bigint);\n if (!entry) {\n throw new SessionError(\n `Unknown incoming TRACK_STATUS ${requestId}`,\n 'INVALID_STATE',\n );\n }\n\n this.incomingTrackStatuses.delete(requestId as bigint);\n\n const requestError: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId,\n errorCode,\n retryInterval: varint(0n),\n errorReason,\n requestKind: 'TRACK_STATUS', // draft-14 → TRACK_STATUS_ERROR; ignored by 16/18\n };\n\n return [this.sendControl(requestError)];\n }\n\n /**\n * Get an incoming TRACK_STATUS request by request ID.\n * @see draft-ietf-moq-transport-16 §9.19\n */\n getIncomingTrackStatus(requestId: bigint): { namespace: Uint8Array[]; name: Uint8Array } | undefined {\n return this.incomingTrackStatuses.get(requestId as bigint);\n }\n\n /**\n * Get a still-pending OUTGOING TRACK_STATUS request (one we sent, awaiting its\n * REQUEST_OK / REQUEST_ERROR). Cleared once the response is handled.\n * @see draft-ietf-moq-transport-16 §9.19\n */\n getPendingTrackStatus(requestId: bigint): { namespace: Uint8Array[]; name: Uint8Array } | undefined {\n return this.pendingTrackStatuses.get(requestId as bigint);\n }\n\n /**\n * Handle a message received on a namespace discovery bidi stream.\n * The adapter routes messages by the requestId associated with the stream.\n * @see draft-ietf-moq-transport-16 §6.1\n */\n /**\n * Handle a FIN / reset on a SUBSCRIBE_NAMESPACE response stream.\n *\n * §6.1 / §10.18: \"When a subscriber receives a stream reset or FIN on a\n * SUBSCRIBE_NAMESPACE response stream, it SHOULD treat this as though each\n * active namespace received a NAMESPACE_DONE.\" We terminate the (subscriber)\n * namespace subscription and drop it. No-op for an unknown / already-terminated\n * request.\n */\n handleNamespaceStreamClosed(requestId: bigint): SessionOutboundAction[] {\n const nsSm = this.namespaceSubscriptions.get(requestId as bigint);\n if (!nsSm) return [];\n if (nsSm.isActive) {\n nsSm.handleNamespaceDone(); // ACTIVE → TERMINATED (treat actives as done)\n }\n this.namespaceSubscriptions.delete(requestId as bigint);\n return [];\n }\n\n handleNamespaceStreamMessage(\n requestId: bigint,\n msg: ControlMessage,\n ): SessionOutboundAction[] {\n const nsSm = this.namespaceSubscriptions.get(requestId as bigint);\n if (!nsSm) {\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${requestId} for namespace stream message`,\n );\n }\n\n switch (msg.type) {\n case 'REQUEST_OK':\n nsSm.handleRequestOk();\n return [];\n\n case 'REQUEST_ERROR': {\n const errMsg = msg as RequestErrorMsg;\n nsSm.handleRequestError(errMsg.errorCode, errMsg.errorReason);\n return [];\n }\n\n case 'NAMESPACE': {\n const nsMsg = msg as Namespace;\n // §2.4.1: Combined prefix + suffix must satisfy namespace constraints\n const nsValidationError = this.validateCombinedNamespace(\n nsSm.namespacePrefix, nsMsg.trackNamespaceSuffix,\n );\n if (nsValidationError) return nsValidationError;\n nsSm.handleNamespace(nsMsg.trackNamespaceSuffix);\n return [];\n }\n\n case 'NAMESPACE_DONE': {\n const ndMsg = msg as NamespaceDone;\n // §2.4.1: Combined prefix + suffix must satisfy namespace constraints\n const ndValidationError = this.validateCombinedNamespace(\n nsSm.namespacePrefix, ndMsg.trackNamespaceSuffix,\n );\n if (ndValidationError) return ndValidationError;\n // §6.1: \"If a subscriber receives a NAMESPACE_DONE before the\n // corresponding NAMESPACE, it MUST close the session with a\n // 'PROTOCOL_VIOLATION'.\"\n if (!nsSm.hasDiscoveredSuffix(ndMsg.trackNamespaceSuffix)) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `NAMESPACE_DONE for suffix not previously announced via NAMESPACE (§6.1)`,\n );\n }\n nsSm.handleNamespaceDone();\n return [];\n }\n\n default:\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `Unexpected message type ${msg.type} on namespace stream`,\n );\n }\n }\n\n /**\n * Get a namespace subscription by request ID.\n */\n getNamespaceSubscription(requestId: bigint): NamespaceStateMachine | undefined {\n return this.namespaceSubscriptions.get(requestId as bigint);\n }\n\n // ─── Namespace Discovery on Control Stream ──────────────────────────\n\n /**\n * Handle incoming PUBLISH_NAMESPACE on the control stream.\n *\n * §6.2 (v14 + v16): \"A subscriber MUST send exactly one\n * [PUBLISH_NAMESPACE_OK / PUBLISH_NAMESPACE_ERROR | REQUEST_OK /\n * REQUEST_ERROR] in response to a PUBLISH_NAMESPACE.\"\n *\n * Draft-14 (§9.23 / §9.25): ack via `PUBLISH_NAMESPACE_OK` /\n * `PUBLISH_NAMESPACE_ERROR (UNINTERESTED 0x4)`.\n * Draft-16 (§6.2 / §9.20): ack via `REQUEST_OK` / `REQUEST_ERROR`.\n *\n * Behavior:\n * - Match found (we previously sent SUBSCRIBE_NAMESPACE with a matching\n * prefix): record namespace, ack OK, notify_namespace under that\n * subscription's requestId.\n * - No match:\n * - v14: ack UNINTERESTED (preserves existing v14 semantics).\n * - v16: ack OK and notify_namespace under the publisher's requestId.\n * Per §6.2 a publisher MAY push PUBLISH_NAMESPACE without a prior\n * SUBSCRIBE_NAMESPACE; the application layer decides interest.\n *\n * @see draft-ietf-moq-transport-14 §9.23, §6.2\n * @see draft-ietf-moq-transport-16 §9.20, §6.2\n */\n private handleIncomingPublishNamespace(msg: PublishNamespace): SessionOutboundAction[] {\n // §2.4.1: validate the Track Namespace BEFORE creating any namespace state.\n const nsError = this.validateNamespace18(msg.trackNamespace);\n if (nsError) return nsError;\n\n // Validate incoming request ID and auto-replenish MAX_REQUEST_ID §9.5\n const validated = this.validateAndReplenish(msg.requestId);\n if (validated.error) return validated.error;\n\n // §3.2: reserved `.`/`.session` namespace → REQUEST_ERROR DOES_NOT_EXIST.\n const reserved = this.rejectReservedNamespace(msg.trackNamespace, msg.requestId, validated.replenish);\n if (reserved) return reserved;\n\n const match = this.findNamespaceSubscriptionByPrefix(msg.trackNamespace);\n const isV14 = this._draftVersion === 14;\n\n if (!match) {\n if (isV14) {\n // §9.25: UNINTERESTED (0x4) — \"The namespace is not of interest.\"\n const errorMsg: PublishNamespaceError = {\n type: 'PUBLISH_NAMESPACE_ERROR',\n requestId: msg.requestId,\n errorCode: varint(0x4n),\n errorReason: 'No matching namespace subscription',\n };\n return [this.sendControl(errorMsg), ...(validated.replenish ?? [])];\n }\n // v16: surface to application + ack OK. The publisher's requestId\n // is the dispatch key (no internal namespace subscription exists).\n const okMsg: RequestOk = {\n type: 'REQUEST_OK',\n requestId: msg.requestId,\n parameters: new Map(),\n };\n const notifyAction: NotifyNamespaceAction = {\n type: 'notify_namespace',\n requestId: msg.requestId,\n message: msg,\n };\n // draft-18: track the inbound announce so a stream FIN/reset can withdraw it.\n if (isRequestStreamDraft(this._draftVersion)) {\n this.incomingPublishNamespaces.set(msg.requestId as bigint, { namespace: msg.trackNamespace });\n }\n return [this.sendControl(okMsg), notifyAction, ...(validated.replenish ?? [])];\n }\n\n // Match found — record the announced namespace.\n match.nsSm.handleNamespace(msg.trackNamespace);\n if (isRequestStreamDraft(this._draftVersion)) {\n this.incomingPublishNamespaces.set(msg.requestId as bigint, { namespace: msg.trackNamespace });\n }\n\n const okMsg: ControlMessage = isV14\n ? { type: 'PUBLISH_NAMESPACE_OK', requestId: msg.requestId }\n : { type: 'REQUEST_OK', requestId: msg.requestId, parameters: new Map() };\n\n const notifyAction: NotifyNamespaceAction = {\n type: 'notify_namespace',\n requestId: match.nsSm.requestId,\n message: msg,\n };\n\n return [this.sendControl(okMsg), notifyAction, ...(validated.replenish ?? [])];\n }\n\n /**\n * Withdraw an inbound PUBLISH_NAMESPACE on a FIN/reset of its request stream\n * (draft-18 §3.3.2 — PUBLISH_NAMESPACE_DONE was removed). If we tracked the\n * announce, drop it and withdraw it from any matching namespace subscription.\n * No-op if no state exists for `requestId` (e.g. already withdrawn). Returns no\n * outbound actions — the withdrawal signal is the stream close itself.\n *\n * @see draft-ietf-moq-transport-18 §3.3.2\n */\n handleInboundPublishNamespaceClosed(requestId: bigint): SessionOutboundAction[] {\n const entry = this.incomingPublishNamespaces.get(requestId as bigint);\n if (!entry) return [];\n this.incomingPublishNamespaces.delete(requestId as bigint);\n const match = this.findNamespaceSubscriptionByPrefix(entry.namespace);\n if (match) match.nsSm.withdrawNamespace(entry.namespace);\n return [];\n }\n\n // ─── Inbound SUBSCRIBE_NAMESPACE (publisher side, draft-18 §10.18) ─────────\n\n /**\n * Handle an incoming SUBSCRIBE_NAMESPACE (we are the publisher). Validates the\n * Request ID (d18 inbound policy) and the prefix, then either:\n * - rejects an overlapping prefix with REQUEST_ERROR / PREFIX_OVERLAP (NOT a\n * session close — the request simply fails); or\n * - creates a PENDING publisher-side {@link NamespaceStateMachine} and returns\n * no response, leaving accept/reject to the application.\n * A malformed prefix or params closes the session with PROTOCOL_VIOLATION.\n *\n * @see draft-ietf-moq-transport-18 §10.18\n */\n private handleIncomingSubscribeNamespace(msg: SubscribeNamespace): SessionOutboundAction[] {\n const validated = this.validateAndReplenish(msg.requestId);\n if (validated.error) return validated.error;\n\n // Malformed prefix (>32 fields or >4096 bytes) → PROTOCOL_VIOLATION.\n try {\n validateTrackNamespacePrefix(msg.trackNamespacePrefix);\n } catch (e) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n e instanceof Error ? e.message : 'Malformed SUBSCRIBE_NAMESPACE prefix',\n );\n }\n\n // §3.2: reserved `.`/`.session` prefix → REQUEST_ERROR DOES_NOT_EXIST.\n const reserved = this.rejectReservedNamespace(msg.trackNamespacePrefix, msg.requestId, validated.replenish);\n if (reserved) return reserved;\n\n // Reject a prefix that overlaps an existing non-terminated incoming sub.\n for (const existing of this.incomingNamespaceSubscriptions.values()) {\n if (existing.isTerminated) continue;\n if (prefixesOverlap(existing.namespacePrefix, msg.trackNamespacePrefix)) {\n const errorMsg: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId: msg.requestId,\n errorCode: RequestErrorCode.PREFIX_OVERLAP,\n retryInterval: varint(0n),\n errorReason: 'Track Namespace Prefix overlaps an existing SUBSCRIBE_NAMESPACE',\n requestKind: 'SUBSCRIBE_NAMESPACE', // draft-14 → SUBSCRIBE_NAMESPACE_ERROR; ignored by 16/18\n };\n return [this.sendControl(errorMsg), ...(validated.replenish ?? [])];\n }\n }\n\n const nsSm = NamespaceStateMachine.createAsPublisher(msg.requestId as bigint, msg.trackNamespacePrefix);\n this.incomingNamespaceSubscriptions.set(msg.requestId as bigint, nsSm);\n // No auto-ack — the application accepts/rejects via accept/rejectSubscribeNamespace.\n return validated.replenish ?? [];\n }\n\n /**\n * Accept an incoming SUBSCRIBE_NAMESPACE: send REQUEST_OK and move the\n * publisher-side machine PENDING → ACTIVE. The request stream stays open for\n * NAMESPACE / NAMESPACE_DONE announcements.\n */\n acceptSubscribeNamespace(requestId: bigint, params: Parameters = new Map()): SessionOutboundAction[] {\n const nsSm = this.incomingNamespaceSubscriptions.get(requestId as bigint);\n if (!nsSm) {\n throw new SessionError(`Unknown incoming SUBSCRIBE_NAMESPACE ${requestId}`, 'INVALID_STATE');\n }\n nsSm.sendRequestOk(); // PENDING → ACTIVE\n const requestOk: RequestOk = { type: 'REQUEST_OK', requestId, parameters: params };\n return [this.sendControl(requestOk)];\n }\n\n /**\n * Reject an incoming SUBSCRIBE_NAMESPACE: send REQUEST_ERROR and drop the\n * publisher-side state. The I/O layer FINs the request stream afterward.\n */\n rejectSubscribeNamespace(requestId: bigint, errorCode: bigint, errorReason: string): SessionOutboundAction[] {\n const nsSm = this.incomingNamespaceSubscriptions.get(requestId as bigint);\n if (!nsSm) {\n throw new SessionError(`Unknown incoming SUBSCRIBE_NAMESPACE ${requestId}`, 'INVALID_STATE');\n }\n this.incomingNamespaceSubscriptions.delete(requestId as bigint);\n const errorMsg: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId,\n errorCode,\n retryInterval: varint(0n),\n errorReason,\n requestKind: 'SUBSCRIBE_NAMESPACE', // draft-14 → SUBSCRIBE_NAMESPACE_ERROR; ignored by 16/18\n };\n return [this.sendControl(errorMsg)];\n }\n\n /**\n * Announce a matching namespace on an accepted incoming SUBSCRIBE_NAMESPACE\n * stream (NAMESPACE, §10.18). The stream stays ACTIVE for further updates.\n */\n sendNamespace(requestId: bigint, suffix: Uint8Array[]): SessionOutboundAction[] {\n const nsSm = this.requireActiveIncomingNamespaceSub(requestId, 'sendNamespace');\n nsSm.sendNamespace(suffix);\n const msg: Namespace = { type: 'NAMESPACE', trackNamespaceSuffix: suffix };\n return [this.sendControl(msg)];\n }\n\n /**\n * Publisher-side: emit NAMESPACE_DONE for a previously announced suffix on an\n * accepted incoming SUBSCRIBE_NAMESPACE stream (§10.18). Locally this is a\n * PER-SUFFIX withdrawal from our publisher bookkeeping (via `withdrawNamespace`,\n * NOT the terminal `NamespaceStateMachine.sendNamespaceDone()`), so our incoming\n * machine stays ACTIVE and may emit further NAMESPACE / NAMESPACE_DONE. The\n * RECEIVING subscriber, however, treats NAMESPACE_DONE as TERMINATING its\n * namespace subscription (§6.1, `handleNamespaceDone`).\n */\n sendNamespaceDone(requestId: bigint, suffix: Uint8Array[]): SessionOutboundAction[] {\n const nsSm = this.requireActiveIncomingNamespaceSub(requestId, 'sendNamespaceDone');\n nsSm.withdrawNamespace(suffix); // per-suffix removal; machine stays ACTIVE\n const msg: NamespaceDone = { type: 'NAMESPACE_DONE', trackNamespaceSuffix: suffix };\n return [this.sendControl(msg)];\n }\n\n private requireActiveIncomingNamespaceSub(requestId: bigint, op: string): NamespaceStateMachine {\n const nsSm = this.incomingNamespaceSubscriptions.get(requestId as bigint);\n if (!nsSm) {\n throw new SessionError(`Unknown incoming SUBSCRIBE_NAMESPACE ${requestId}`, 'INVALID_STATE');\n }\n if (!nsSm.isActive) {\n throw new SessionError(`Cannot ${op} for SUBSCRIBE_NAMESPACE ${requestId}: not accepted`, 'INVALID_STATE');\n }\n return nsSm;\n }\n\n /** Whether we are serving an accepted incoming SUBSCRIBE_NAMESPACE. */\n getIncomingNamespaceSubscription(requestId: bigint): NamespaceStateMachine | undefined {\n return this.incomingNamespaceSubscriptions.get(requestId as bigint);\n }\n\n /**\n * Cancel an incoming SUBSCRIBE_NAMESPACE on a FIN/reset of its request stream\n * (draft-18 §10.18). Drops publisher-side state; no-op if none exists. The\n * cancellation signal is the stream close itself, so no outbound actions.\n */\n handleInboundSubscribeNamespaceClosed(requestId: bigint): SessionOutboundAction[] {\n this.incomingNamespaceSubscriptions.delete(requestId as bigint);\n return [];\n }\n\n // ─── Inbound SUBSCRIBE_TRACKS (publisher side, draft-18 §10.19) ────────────\n\n /**\n * Handle an incoming SUBSCRIBE_TRACKS (we are the publisher). Validates the\n * Request ID (d18 inbound policy) and the prefix, then either:\n * - rejects an overlapping prefix — only against OTHER incoming\n * SUBSCRIBE_TRACKS, never SUBSCRIBE_NAMESPACE — with REQUEST_ERROR /\n * PREFIX_OVERLAP (NOT a session close); or\n * - records a PENDING incoming track subscription and returns no response,\n * leaving accept/reject to the application.\n * A malformed prefix closes the session with PROTOCOL_VIOLATION.\n *\n * @see draft-ietf-moq-transport-18 §10.19\n */\n private handleIncomingSubscribeTracks(msg: SubscribeTracks): SessionOutboundAction[] {\n const validated = this.validateAndReplenish(msg.requestId);\n if (validated.error) return validated.error;\n\n const filterError = this.rejectUnsupportedRangeFilters(msg.requestId, msg.parameters);\n if (filterError) return filterError;\n\n try {\n validateTrackNamespacePrefix(msg.trackNamespacePrefix);\n } catch (e) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n e instanceof Error ? e.message : 'Malformed SUBSCRIBE_TRACKS prefix',\n );\n }\n\n // §3.2: reserved `.`/`.session` prefix → REQUEST_ERROR DOES_NOT_EXIST.\n const reserved = this.rejectReservedNamespace(msg.trackNamespacePrefix, msg.requestId, validated.replenish);\n if (reserved) return reserved;\n\n // Overlap is checked ONLY against other incoming SUBSCRIBE_TRACKS — a\n // SUBSCRIBE_TRACKS prefix may coexist with a SUBSCRIBE_NAMESPACE prefix.\n for (const existing of this.incomingTrackSubscriptions.values()) {\n if (prefixesOverlap(existing.trackNamespacePrefix, msg.trackNamespacePrefix)) {\n const errorMsg: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId: msg.requestId,\n errorCode: RequestErrorCode.PREFIX_OVERLAP,\n retryInterval: varint(0n),\n errorReason: 'Track Namespace Prefix overlaps an existing SUBSCRIBE_TRACKS',\n };\n return [this.sendControl(errorMsg), ...(validated.replenish ?? [])];\n }\n }\n\n this.incomingTrackSubscriptions.set(msg.requestId as bigint, {\n trackNamespacePrefix: msg.trackNamespacePrefix,\n state: 'pending',\n parameters: new Map(msg.parameters),\n forward: isDraft22(this._draftVersion) && msg.parameters.get(MessageParam.FORWARD)?.[0] === 0n\n ? ForwardState.PAUSED : ForwardState.ACTIVE,\n });\n return validated.replenish ?? [];\n }\n\n /**\n * Accept an incoming SUBSCRIBE_TRACKS: send REQUEST_OK and mark it ACTIVE. The\n * request stream stays open for PUBLISH (new streams) and PUBLISH_BLOCKED.\n */\n acceptSubscribeTracks(requestId: bigint, params: Parameters = new Map()): SessionOutboundAction[] {\n const ts = this.incomingTrackSubscriptions.get(requestId as bigint);\n if (!ts) {\n throw new SessionError(`Unknown incoming SUBSCRIBE_TRACKS ${requestId}`, 'INVALID_STATE');\n }\n ts.state = 'active';\n const requestOk: RequestOk = { type: 'REQUEST_OK', requestId, parameters: params };\n return [this.sendControl(requestOk)];\n }\n\n /**\n * Reject an incoming SUBSCRIBE_TRACKS: send REQUEST_ERROR and drop state. The\n * I/O layer FINs the request stream afterward.\n */\n rejectSubscribeTracks(requestId: bigint, errorCode: bigint, errorReason: string): SessionOutboundAction[] {\n const ts = this.incomingTrackSubscriptions.get(requestId as bigint);\n if (!ts) {\n throw new SessionError(`Unknown incoming SUBSCRIBE_TRACKS ${requestId}`, 'INVALID_STATE');\n }\n this.incomingTrackSubscriptions.delete(requestId as bigint);\n const errorMsg: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId,\n errorCode,\n retryInterval: varint(0n),\n errorReason,\n };\n return [this.sendControl(errorMsg)];\n }\n\n /**\n * Indicate a Track cannot be served within an accepted incoming\n * SUBSCRIBE_TRACKS (PUBLISH_BLOCKED, §10.20). Valid only after the request is\n * ACTIVE; the stream stays open (no seal/close).\n */\n sendPublishBlocked(requestId: bigint, suffix: Uint8Array[], trackName: Uint8Array): SessionOutboundAction[] {\n const ts = this.incomingTrackSubscriptions.get(requestId as bigint);\n if (!ts) {\n throw new SessionError(`Unknown incoming SUBSCRIBE_TRACKS ${requestId}`, 'INVALID_STATE');\n }\n if (ts.state !== 'active') {\n throw new SessionError(`Cannot sendPublishBlocked for SUBSCRIBE_TRACKS ${requestId}: not accepted`, 'INVALID_STATE');\n }\n const msg: PublishBlocked = { type: 'PUBLISH_BLOCKED', trackNamespaceSuffix: suffix, trackName };\n return [this.sendControl(msg)];\n }\n\n /** Whether we are serving an incoming SUBSCRIBE_TRACKS (pending or active). */\n getIncomingTrackSubscription(requestId: bigint): { trackNamespacePrefix: Uint8Array[]; state: 'pending' | 'active'; forward: ForwardStateValue } | undefined {\n return this.incomingTrackSubscriptions.get(requestId as bigint);\n }\n\n /**\n * Cancel an incoming SUBSCRIBE_TRACKS on a FIN/reset of its request stream\n * (draft-18 §10.19). Drops publisher-side state; no-op if none exists.\n */\n handleInboundSubscribeTracksClosed(requestId: bigint): SessionOutboundAction[] {\n this.incomingTrackSubscriptions.delete(requestId as bigint);\n return [];\n }\n\n /**\n * Cancel an inbound PUBLISH / SUBSCRIBE / FETCH / TRACK_STATUS on a FIN/reset of its request\n * stream (draft-18 §3.3.2) — a normal lifecycle end, not a protocol error. Drops\n * the publisher-/subscriber-side request state; no-op if none exists. UNREGISTERS\n * the request's Track Alias from the session registry so a later request may\n * reuse it (an inbound PUBLISH registered its publisher-chosen alias at receipt,\n * §9.13). The I/O layer keeps its own bounded late-object guard — a separate,\n * adapter-level concern.\n */\n handleInboundRequestClosed(requestId: bigint): SessionOutboundAction[] {\n const sub = this.incomingSubscriptions.get(requestId as bigint);\n if (sub?.trackAlias !== undefined) this.trackAliases.unregister(sub.trackAlias, sub.requestId);\n this.incomingSubscriptions.delete(requestId as bigint);\n this.incomingJoiningLocations.delete(requestId as bigint);\n this.incomingFetches.delete(requestId as bigint);\n this.incomingTrackStatuses.delete(requestId as bigint);\n // §5.1: the PUBLISH went away before acceptance — discard any staged collision\n // so the local SUBSCRIBE it would have superseded stays alive.\n this.pendingCollisions.delete(requestId as bigint);\n // §11.4.1: the request stream is gone — drop any REQUEST_UPDATE still pending\n // against it, else its record (and REQUEST_OK expectation) leaks forever.\n this.dropPendingUpdatesFor(requestId);\n return [];\n }\n\n /** Drop every pending REQUEST_UPDATE that targets the (now-gone) request `targetId`. */\n private dropPendingUpdatesFor(targetId: bigint): void {\n for (const [updateId, pending] of [...this.pendingUpdates]) {\n if (pending.existingRequestId === (targetId as bigint)) this.pendingUpdates.delete(updateId);\n }\n }\n\n /**\n * Clean local state for an OUTBOUND request whose bidi request stream the PEER\n * closed (FIN/reset). §11.4.1: \"Termination of a bidi request stream terminates\n * the Subscription, Fetch, Track Status, Publish Namespace, or Subscribe\n * Namespace request.\" Drops every map keyed by this Request ID and unregisters a\n * SUBSCRIBE's Track Alias. No-op for a request already ended. (The continuing\n * SUBSCRIBE_NAMESPACE / SUBSCRIBE_TRACKS streams have their own closed handlers —\n * {@link handleNamespaceStreamClosed} / {@link handleSubscribeTracksStreamClosed}.)\n *\n * @see draft-ietf-moq-transport-18 §11.4.1\n */\n /**\n * Roll back an OUTBOUND request that failed before/at emission.\n *\n * Two distinct failure classes require different provenance handling:\n * - Ordinary rollback (default): the request's bytes never left this endpoint\n * (e.g. a pre-send ownership callback threw). No response can ever arrive,\n * so all state — including any cancellation shadow — is dropped.\n * - `retainCancellationProvenance`: the send FAILED AMBIGUOUSLY — bytes may\n * have reached the peer, so a response may still cross. The request's kind\n * and phase are recorded in `recentlyCancelled` BEFORE cleanup, so a crossed\n * SUBSCRIBE_OK / FETCH_OK / REQUEST_ERROR / PUBLISH_DONE is tolerated as\n * known-cancelled instead of closing the session as unknown-request.\n */\n rollbackRequest(\n requestId: bigint,\n options?: { retainCancellationProvenance?: boolean },\n ): void {\n const retain = options?.retainCancellationProvenance === true;\n const sub = this.subscriptions.get(requestId as bigint);\n if (retain) {\n if (sub) this.recordCancellation(requestId, 'subscribe', !sub.isPending);\n else if (this.fetches.has(requestId as bigint)) this.recordCancellation(requestId, 'fetch', false);\n }\n if (sub) {\n if (sub.trackAlias !== undefined) this.trackAliases.unregister(sub.trackAlias, sub.requestId);\n this.subscriptions.delete(requestId as bigint);\n this.subscriptionTracks.delete(requestId as bigint);\n }\n this.fetches.delete(requestId as bigint);\n this.pendingTrackStatuses.delete(requestId as bigint);\n this.outgoingPublishes.delete(requestId as bigint);\n this.incomingJoiningLocations.delete(requestId as bigint);\n this.publishedNamespaces.delete(requestId as bigint);\n this.dropPendingUpdatesFor(requestId);\n if (!retain) this.recentlyCancelled.delete(requestId as bigint);\n }\n\n /** Roll back a proven-unsent request and reclaim its latest allocation. */\n rollbackUnsentRequest(requestId: bigint): boolean {\n this.rollbackRequest(requestId);\n return this.requestIdAllocator.releaseUnsent(requestId);\n }\n\n /**\n * Roll back an outbound PUBLISH whose bytes were proven not to leave this\n * endpoint. Legacy drafts require gap-free Request IDs, so the allocation is\n * reusable only when no later request was allocated reentrantly.\n */\n rollbackUnsentPublish(requestId: bigint): boolean {\n if (!this.outgoingPublishes.has(requestId as bigint)) return false;\n return this.rollbackUnsentRequest(requestId);\n }\n\n handleOutboundRequestClosed(requestId: bigint): SessionOutboundAction[] {\n const sub = this.subscriptions.get(requestId as bigint);\n if (sub) {\n if (sub.trackAlias !== undefined) this.trackAliases.unregister(sub.trackAlias, sub.requestId);\n this.subscriptions.delete(requestId as bigint);\n this.subscriptionTracks.delete(requestId as bigint);\n }\n this.fetches.delete(requestId as bigint);\n this.pendingTrackStatuses.delete(requestId as bigint);\n this.outgoingPublishes.delete(requestId as bigint);\n this.incomingJoiningLocations.delete(requestId as bigint);\n this.publishedNamespaces.delete(requestId as bigint);\n // §11.4.1: drop any REQUEST_UPDATE still pending against this gone request.\n this.dropPendingUpdatesFor(requestId);\n // draft-18: the request stream is gone — no crossed terminal can arrive now, so\n // reclaim any cancellation provenance for it (keeps the bounded map lean).\n this.recentlyCancelled.delete(requestId as bigint);\n return [];\n }\n\n /**\n * Handle PUBLISH_NAMESPACE_DONE on the control stream.\n *\n * Per-namespace withdrawal — the subscription stays ACTIVE.\n *\n * Wire format diverges between drafts:\n * - v14 (§9.26): includes the Track Namespace tuple — lookup by prefix.\n * - v16 (§9.22, Figure 23): includes only the Request ID of the\n * original PUBLISH_NAMESPACE — surface to application unchanged.\n *\n * @see draft-ietf-moq-transport-14 §9.26\n * @see draft-ietf-moq-transport-16 §9.22\n */\n private handlePublishNamespaceDone(msg: PublishNamespaceDone): SessionOutboundAction[] {\n if (this._draftVersion === 14) {\n const namespace = msg.trackNamespace;\n if (!namespace) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n 'PUBLISH_NAMESPACE_DONE on control stream must include trackNamespace (v14)',\n );\n }\n\n const match = this.findNamespaceSubscriptionByPrefix(namespace);\n if (!match) return [];\n\n match.nsSm.withdrawNamespace(namespace);\n\n const notifyAction: NotifyNamespaceAction = {\n type: 'notify_namespace',\n requestId: match.nsSm.requestId,\n message: msg,\n };\n return [notifyAction];\n }\n\n // v16: keyed by requestId of the original PUBLISH_NAMESPACE.\n if (msg.requestId === undefined) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n 'PUBLISH_NAMESPACE_DONE on control stream must include requestId (v16)',\n );\n }\n const notifyAction: NotifyNamespaceAction = {\n type: 'notify_namespace',\n requestId: msg.requestId,\n message: msg,\n };\n return [notifyAction];\n }\n\n /**\n * Handle PUBLISH_NAMESPACE_CANCEL on the control stream (draft-14 only).\n *\n * Per-namespace withdrawal with error info. Subscription stays ACTIVE.\n *\n * @see draft-ietf-moq-transport-14 §9.27\n */\n private handlePublishNamespaceCancel(msg: PublishNamespaceCancel): SessionOutboundAction[] {\n const namespace = msg.trackNamespace;\n if (!namespace) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n 'PUBLISH_NAMESPACE_CANCEL on control stream must include trackNamespace',\n );\n }\n\n const match = this.findNamespaceSubscriptionByPrefix(namespace);\n if (!match) {\n return [];\n }\n\n match.nsSm.withdrawNamespace(namespace);\n\n const notifyAction: NotifyNamespaceAction = {\n type: 'notify_namespace',\n requestId: match.nsSm.requestId,\n message: msg,\n };\n return [notifyAction];\n }\n\n /**\n * Handle PUBLISH_NAMESPACE_OK on the control stream (draft-14 only).\n *\n * Response to our publishNamespace() — peer accepted the namespace.\n * Resolves the pending publish namespace.\n *\n * @see draft-ietf-moq-transport-14 §9.24: \"The subscriber sends a\n * PUBLISH_NAMESPACE_OK control message to acknowledge the successful\n * authorization and acceptance of a PUBLISH_NAMESPACE message.\"\n * @see draft-ietf-moq-transport-14 §6.2: \"A subscriber MUST send exactly\n * one PUBLISH_NAMESPACE_OK or PUBLISH_NAMESPACE_ERROR in response to\n * a PUBLISH_NAMESPACE.\"\n */\n private handlePublishNamespaceOk(msg: PublishNamespaceOk): SessionOutboundAction[] {\n const pubNs = this.publishedNamespaces.get(msg.requestId as bigint);\n if (pubNs && pubNs.state === 'pending') {\n pubNs.state = 'active';\n return [];\n }\n\n // §9.1: Unknown request ID or duplicate response\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${msg.requestId} for PUBLISH_NAMESPACE_OK`,\n );\n }\n\n /**\n * Handle PUBLISH_NAMESPACE_ERROR on the control stream (draft-14 only).\n *\n * Response to our publishNamespace() — peer rejected the namespace.\n * Resolves the pending publish namespace.\n *\n * @see draft-ietf-moq-transport-14 §9.25: \"The subscriber sends a\n * PUBLISH_NAMESPACE_ERROR control message for tracks that failed\n * authorization.\"\n *\n * Error codes per §9.25:\n * INTERNAL_ERROR (0x0), UNAUTHORIZED (0x1), TIMEOUT (0x2),\n * NOT_SUPPORTED (0x3), UNINTERESTED (0x4),\n * MALFORMED_AUTH_TOKEN (0x10), EXPIRED_AUTH_TOKEN (0x12)\n */\n private handlePublishNamespaceError(msg: PublishNamespaceError): SessionOutboundAction[] {\n const pubNs = this.publishedNamespaces.get(msg.requestId as bigint);\n if (pubNs && pubNs.state === 'pending') {\n this.publishedNamespaces.delete(msg.requestId as bigint);\n return [];\n }\n\n // §9.1: Unknown request ID or duplicate response\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown request ID ${msg.requestId} for PUBLISH_NAMESPACE_ERROR`,\n );\n }\n\n /**\n * Handle incoming UNSUBSCRIBE_NAMESPACE on the control stream (draft-14 only).\n *\n * The subscriber is cancelling a previous SUBSCRIBE_NAMESPACE.\n * In draft-16, this is replaced by closing the bidi stream (§6.1).\n *\n * Currently we don't track publisher-side namespace subscriptions\n * received on the control stream, so this is a graceful no-op that\n * prevents falling through to the PROTOCOL_VIOLATION default handler.\n *\n * @see draft-ietf-moq-transport-14 §9.31: \"A subscriber issues a\n * UNSUBSCRIBE_NAMESPACE message to a publisher indicating it is no\n * longer interested in PUBLISH_NAMESPACE, PUBLISH_NAMESPACE_DONE and\n * PUBLISH messages for the specified track namespace prefix.\"\n */\n private handleIncomingUnsubscribeNamespace(_msg: UnsubscribeNamespace): SessionOutboundAction[] {\n // Graceful no-op: publisher-side SUBSCRIBE_NAMESPACE handling on the\n // control stream is not implemented (draft-14 only, §9.28–§9.31).\n // The important thing is NOT to close the session with PROTOCOL_VIOLATION.\n return [];\n }\n\n /**\n * Cancel a namespace subscription (draft-14 only).\n *\n * Sends UNSUBSCRIBE_NAMESPACE and terminates the state machine.\n *\n * @see draft-ietf-moq-transport-14 §9.31: \"A subscriber issues a\n * UNSUBSCRIBE_NAMESPACE message to a publisher indicating it is no\n * longer interested.\"\n */\n cancelNamespace(requestId: bigint): SessionOutboundAction[] {\n const nsSm = this.namespaceSubscriptions.get(requestId as bigint);\n if (!nsSm) {\n throw new SessionError(`Unknown namespace subscription ${requestId}`, 'INVALID_STATE');\n }\n\n const unsubMsg: UnsubscribeNamespace = {\n type: 'UNSUBSCRIBE_NAMESPACE',\n trackNamespacePrefix: nsSm.namespacePrefix,\n };\n\n // Terminate the state machine\n nsSm.handleNamespaceDone();\n\n return [this.sendControl(unsubMsg)];\n }\n\n /**\n * Find the namespace subscription whose prefix matches the given namespace.\n * A namespace matches if it starts with the subscription's prefix.\n *\n * O(n) scan over active namespace subscriptions — typically 1-3.\n */\n private findNamespaceSubscriptionByPrefix(\n namespace: Uint8Array[],\n ): { nsSm: NamespaceStateMachine } | undefined {\n for (const nsSm of this.namespaceSubscriptions.values()) {\n if (!nsSm.isActive) continue;\n\n const prefix = nsSm.namespacePrefix;\n if (namespace.length < prefix.length) continue;\n\n let matches = true;\n for (let i = 0; i < prefix.length; i++) {\n const a = prefix[i]!, b = namespace[i]!;\n if (a.length !== b.length) { matches = false; break; }\n for (let j = 0; j < a.length; j++) {\n if (a[j] !== b[j]) { matches = false; break; }\n }\n if (!matches) break;\n }\n\n if (matches) return { nsSm };\n }\n return undefined;\n }\n\n // ─── Publisher-Side Operations ───────────────────────────────────────\n\n /**\n * Accept an incoming subscription request.\n * Sends SUBSCRIBE_OK and transitions the subscription to ESTABLISHED.\n *\n * `options` (draft-18) may carry SUBSCRIBE_OK `parameters` and `trackProperties`\n * (§2.5); non-empty Track Properties on draft-14/16 throw. The two-argument form\n * remains valid.\n *\n * @see draft-ietf-moq-transport-16 §9.10, draft-ietf-moq-transport-18 §10.4\n */\n acceptSubscribe(\n requestId: bigint,\n trackAlias: bigint,\n options: { parameters?: Parameters; trackProperties?: TrackProperties } = {},\n ): SessionOutboundAction[] {\n const sub = this.incomingSubscriptions.get(requestId as bigint);\n if (!sub) {\n throw new SessionError(\n `Unknown incoming subscription ${requestId}`,\n 'INVALID_STATE',\n );\n }\n\n if (sub.isPublishInitiated) {\n // PUBLISH_OK / REQUEST_OK is the subscriber's Forward-State authority.\n // Reject local misuse before sendSubscribeOk() establishes the state\n // machine or installs any alias authority.\n this.assertLocalForwardParameter(options.parameters, 'PUBLISH acceptance');\n if (isDraft22(this._draftVersion)) {\n const error = this.validateMessageParams(options.parameters ?? new Map(), 'PUBLISH_OK');\n if (error) throw new SessionError(error.reason, 'INVALID_STATE');\n }\n }\n\n // Validate ALL misuse up-front, BEFORE any state mutation: draft-14/16 Track\n // Properties, and Track Properties on a PUBLISH acceptance (which is a\n // REQUEST_OK / PUBLISH_OK, not a SUBSCRIBE_OK — §10.10). sendSubscribeOk()\n // mutates the subscription to ESTABLISHED, so it must run only after these\n // checks pass.\n const trackProperties = this.resolveTrackProperties(options.trackProperties, 'SUBSCRIBE_OK');\n if (sub.isPublishInitiated && trackProperties.size > 0) {\n throw new SessionError(\n 'Track Properties are not valid on a PUBLISH acceptance (only SUBSCRIBE_OK / TRACK_STATUS_OK / FETCH_OK)',\n 'INVALID_STATE',\n );\n }\n\n if (!sub.isPublishInitiated && isDraft22(this._draftVersion)) {\n this.assertPublisherAliasAvailable(trackAlias, sub.trackNamespace!, sub.trackName!);\n const error = this.validateMessageParams(options.parameters ?? new Map(), 'SUBSCRIBE_OK');\n if (error) throw new SessionError(error.reason, 'INVALID_STATE');\n }\n\n sub.sendSubscribeOk(trackAlias);\n\n // PUBLISH-initiated subscriptions respond with a publish-acceptance message\n // (PUBLISH_OK / REQUEST_OK), not SUBSCRIBE_OK.\n if (sub.isPublishInitiated) {\n // §5.1: if a pending local SUBSCRIBE for this track was superseded by this\n // PUBLISH, it MUST reach Terminated BEFORE PUBLISH_OK. Perform the staged\n // cancellation now and emit its actions ahead of the acceptance.\n const preActions = this.cancelStagedCollision(requestId);\n // If the cancellation itself forced a session close (superseded-set at\n // capacity), return ONLY the close — do NOT also append an acceptance.\n if (preActions.some((a) => a.type === 'close_connection')) return preActions;\n let responseParameters: Parameters;\n if (isRequestStreamDraft(this._draftVersion)) {\n responseParameters = options.parameters ?? new Map();\n } else {\n // Draft-14/16 §9.14: derived initial-state fields first, with caller-\n // supplied response parameters taking precedence.\n responseParameters = this.buildPublishOkParamsFromPublish(sub.publishParameters);\n for (const [key, values] of options.parameters ?? new Map()) {\n responseParameters.set(key, values);\n }\n }\n\n // Apply the exact value communicated on the wire, including the default\n // of 1 when FORWARD is absent, so the two endpoints cannot disagree.\n const acceptFwd = responseParameters.get(MessageParam.FORWARD)?.[0];\n if (!isDraft22(this._draftVersion)) {\n sub.updateForwardState(\n typeof acceptFwd === 'bigint' && acceptFwd === 0n\n ? ForwardState.PAUSED\n : ForwardState.ACTIVE,\n );\n }\n\n if (isRequestStreamDraft(this._draftVersion)) {\n // draft-18 §10.10: PUBLISH_OK is REQUEST_OK shorthand (wire 0x07, no\n // Request ID); the I/O layer writes it on the inbound PUBLISH request\n // stream, not the control stream. Caller-supplied response parameters\n // ride it verbatim.\n const requestOk: RequestOk = {\n type: 'REQUEST_OK',\n requestId,\n parameters: responseParameters,\n };\n return [...preActions, this.sendControl(requestOk)];\n }\n const publishOk: PublishOk = {\n type: 'PUBLISH_OK',\n requestId,\n parameters: responseParameters,\n };\n return [...preActions, this.sendControl(publishOk)];\n }\n\n // §5.1: SAVE the Largest Location communicated in this SUBSCRIBE_OK — it\n // becomes the subscription's Joining Location, the mandatory anchor for\n // any Joining FETCH while the subscription is active.\n const communicated = this.extractLargestObjectParam(options.parameters);\n if (communicated) this.incomingJoiningLocations.set(requestId as bigint, communicated);\n\n if (isDraft22(this._draftVersion)) {\n sub.publisherGroupOrder = trackProperties.get(0x22n)?.[0] === 2n ? 'descending' : 'ascending';\n sub.locationWindow = subscriptionWindow(\n sub.currentFilter === undefined ? undefined : decodeSubscriptionFilter(sub.currentFilter, 22), communicated ?? undefined,\n );\n }\n\n const subscribeOk: SubscribeOk = {\n type: 'SUBSCRIBE_OK',\n requestId,\n trackAlias,\n parameters: options.parameters ?? new Map(),\n trackProperties,\n };\n\n return [this.sendControl(subscribeOk)];\n }\n\n /**\n * Reject an incoming subscription request.\n * Sends REQUEST_ERROR and transitions the subscription to TERMINATED.\n * @see draft-ietf-moq-transport-16 §9.8\n */\n rejectSubscribe(requestId: bigint, errorCode: bigint, errorReason: string): SessionOutboundAction[] {\n const sub = this.incomingSubscriptions.get(requestId as bigint);\n if (!sub) {\n throw new SessionError(\n `Unknown incoming subscription ${requestId}`,\n 'INVALID_STATE',\n );\n }\n\n sub.sendRequestError(errorCode, errorReason);\n\n // §5.1: rejecting the PUBLISH discards any staged collision — the local\n // SUBSCRIBE it would have superseded stays ALIVE (never accepted PUBLISH_OK).\n this.pendingCollisions.delete(requestId as bigint);\n\n // Release the Track Alias so a later request may reuse it. An inbound PUBLISH\n // registered its publisher-chosen alias at receipt (§9.13); rejecting it must\n // unregister so a subsequent PUBLISH on the same alias is a per-request matter,\n // not a session-fatal DUPLICATE_TRACK_ALIAS. (No-op for an incoming SUBSCRIBE\n // rejected before acceptSubscribe assigned an alias.)\n if (sub.trackAlias !== undefined) this.trackAliases.unregister(sub.trackAlias, sub.requestId);\n\n // REQUEST_ERROR is the request's first AND terminal response — RECLAIM the\n // incoming subscription state (bounded; a peer cannot grow the map with valid\n // unique requests the application rejects). The adapter's stream teardown seals\n // the context, so its later close callback will not run this cleanup.\n this.incomingSubscriptions.delete(requestId as bigint);\n this.incomingJoiningLocations.delete(requestId as bigint);\n\n // PUBLISH-initiated subscriptions respond with a publish-rejection message:\n // draft-14 PUBLISH_ERROR, drafts 16/18 REQUEST_ERROR (§10.10). Shared with the\n // §5.1 duplicate path so both get the draft split right (PUBLISH_ERROR is a\n // draft-14-only type and is unencodable on draft-16).\n if (sub.isPublishInitiated) {\n return [this.buildPublishReject(requestId, errorCode, errorReason)];\n }\n\n const requestError: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId,\n errorCode,\n retryInterval: varint(0n),\n errorReason,\n requestKind: 'SUBSCRIBE', // draft-14 → SUBSCRIBE_ERROR; ignored by 16/18\n };\n\n return [this.sendControl(requestError)];\n }\n\n /**\n * Handle PUBLISH_DONE received on an inbound PUBLISH stream (draft-18 §10.11):\n * terminate the PUBLISH-initiated subscription and UNREGISTER its Track Alias\n * (owner-conditional) so the alias may be reused. The I/O layer removes its\n * routing immediately and installs a bounded early-discard guard for any late\n * streams — it does NOT retain routing indefinitely.\n */\n handleInboundPublishDone(requestId: bigint): SessionOutboundAction[] {\n const sub = this.incomingSubscriptions.get(requestId as bigint);\n if (!sub) {\n return this.closeWithError(\n SessionErrorCode.INVALID_REQUEST_ID,\n `Unknown PUBLISH ${requestId} for PUBLISH_DONE`,\n );\n }\n // Release the publisher-chosen Track Alias from the session registry so a\n // later PUBLISH may reuse it (the I/O layer keeps its own bounded tombstone\n // to discard late streams — that is a separate, adapter-level concern).\n if (sub.trackAlias !== undefined) this.trackAliases.unregister(sub.trackAlias, sub.requestId);\n this.incomingSubscriptions.delete(requestId as bigint);\n this.incomingJoiningLocations.delete(requestId as bigint);\n // §5.1: PUBLISH ended before acceptance — discard any staged collision.\n this.pendingCollisions.delete(requestId as bigint);\n return [];\n }\n\n /**\n * Build a REQUEST_UPDATE for a PUBLISH-initiated (incoming) subscription\n * (draft-18). The publisher opened the stream; as the subscriber we update the\n * subscription (e.g. FORWARD) by writing REQUEST_UPDATE on that PUBLISH stream.\n * The matching REQUEST_OK / REQUEST_ERROR applies the update (see the REQUEST_OK\n * handler, which also looks up incoming subscriptions).\n */\n updateIncomingSubscription(\n existingRequestId: bigint,\n options: RequestUpdateOptions = {},\n ): RequestResult {\n this.assertEstablishedOrDraining('updateIncomingSubscription');\n const sub = this.incomingSubscriptions.get(existingRequestId as bigint);\n if (!sub) {\n throw new SessionError(\n `Unknown incoming subscription ${existingRequestId} for REQUEST_UPDATE`,\n 'INVALID_STATE',\n );\n }\n if (sub.state !== 'established') {\n throw new SessionError(\n `Cannot update incoming subscription in state ${sub.state}; expected established`,\n 'INVALID_STATE',\n );\n }\n\n const requestedForward = this.normalizeLocalForwardState(options.forward, 'REQUEST_UPDATE');\n if (options.fill !== undefined && !isDraft22(this._draftVersion)) {\n throw new RangeError(`FILL_PARAMETERS needs draft 22; this session speaks draft ${this._draftVersion}`);\n }\n if (isDraft22(this._draftVersion) && this._peerMaxRequestUpdates > 0n) {\n const pending = [...this.pendingUpdates.values()].filter((p) => p.existingRequestId === existingRequestId).length;\n if (BigInt(pending) >= this._peerMaxRequestUpdates) throw new SessionError('MAX_REQUEST_UPDATES exceeded', 'INVALID_STATE');\n }\n const filterBytes = options.subscriptionFilter === undefined ? undefined : encodeSubscriptionFilter(options.subscriptionFilter, this._draftVersion);\n const fillBytes = options.fill === undefined ? undefined : encodeFillParameters(options.fill.filter, options.fill.groupOrder);\n const parameters = authorizationParameters(options);\n const requestId = this.requestIdAllocator.allocate();\n if (filterBytes !== undefined) parameters.set(MessageParam.SUBSCRIPTION_FILTER, [filterBytes]);\n if (fillBytes !== undefined) {\n parameters.set(FILL_PARAMETERS, [fillBytes]);\n this.registerFillRequest(requestId, sub, options.fill!, options.subscriptionFilter);\n }\n if (requestedForward !== undefined) {\n parameters.set(MessageParam.FORWARD, [varint(BigInt(requestedForward))]);\n }\n if (options.subscriberPriority !== undefined) {\n parameters.set(MessageParam.SUBSCRIBER_PRIORITY, [options.subscriberPriority]);\n }\n // §8: a PUBLISH-initiated subscription can carry delivery-timeout updates too.\n if (options.objectDeliveryTimeout !== undefined) {\n parameters.set(MessageParam.OBJECT_DELIVERY_TIMEOUT, [options.objectDeliveryTimeout]);\n }\n if (options.subgroupDeliveryTimeout !== undefined) {\n parameters.set(MessageParam.SUBGROUP_DELIVERY_TIMEOUT, [options.subgroupDeliveryTimeout]);\n }\n\n // Stage ALL changes on the pending record — applied on REQUEST_OK, never at send.\n this.pendingUpdates.set(requestId as bigint, {\n existingRequestId,\n ...(isDraft22(this._draftVersion) && filterBytes !== undefined ? { subscriptionFilter: filterBytes } : {}),\n ...(requestedForward !== undefined\n ? { forward: requestedForward }\n : {}),\n ...(options.objectDeliveryTimeout !== undefined\n ? { objectDeliveryTimeoutMs: Number(options.objectDeliveryTimeout) }\n : {}),\n ...(options.subgroupDeliveryTimeout !== undefined\n ? { subgroupDeliveryTimeoutMs: Number(options.subgroupDeliveryTimeout) }\n : {}),\n });\n\n const updateMsg: RequestUpdate = { type: 'REQUEST_UPDATE', requestId, existingRequestId, parameters };\n return { requestId, actions: [this.sendControl(updateMsg)] };\n }\n\n /**\n * Send PUBLISH_DONE for an established subscription.\n * Terminates the subscription from the publisher side.\n * @see draft-ietf-moq-transport-16 §9.15\n */\n publishDone(requestId: bigint, statusCode: Varint, errorReason: string): SessionOutboundAction[] {\n // PUBLISH_DONE applies to a publisher-side subscription — either one we serve\n // for a peer SUBSCRIBE (incomingSubscriptions) or one WE initiated via an\n // outbound PUBLISH (outgoingPublishes, draft-18 §10.10).\n const sub = this.incomingSubscriptions.get(requestId as bigint)\n ?? this.outgoingPublishes.get(requestId as bigint);\n if (!sub) {\n throw new SessionError(\n `Unknown incoming subscription ${requestId} for PUBLISH_DONE`,\n 'INVALID_STATE',\n );\n }\n\n sub.sendPublishDone(statusCode, errorReason);\n // §10.11: DONE terminates the subscription — reclaim publisher-side state\n // (a duplicate publishDone for the same request then throws Unknown).\n this.outgoingPublishes.delete(requestId as bigint);\n this.incomingSubscriptions.delete(requestId as bigint);\n this.incomingJoiningLocations.delete(requestId as bigint);\n\n const publishDoneMsg: PublishDone = {\n type: 'PUBLISH_DONE',\n requestId,\n statusCode,\n streamCount: sub.streamCount, // §9.15: number of data streams opened\n errorReason,\n };\n\n return [this.sendControl(publishDoneMsg)];\n }\n\n /**\n * Get an incoming subscription (publisher-side) by request ID.\n */\n getIncomingSubscription(requestId: bigint): SubscriptionStateMachine | undefined {\n return this.incomingSubscriptions.get(requestId as bigint);\n }\n\n /**\n * Accept an incoming fetch request.\n * Sends FETCH_OK and transitions the fetch to TRANSFERRING.\n * @see draft-ietf-moq-transport-16 §9.17\n */\n acceptFetch(requestId: bigint, options: FetchAcceptOptions = {}): SessionOutboundAction[] {\n const fetch = this.incomingFetches.get(requestId as bigint);\n if (!fetch) {\n throw new SessionError(\n `Unknown incoming fetch ${requestId}`,\n 'INVALID_STATE',\n );\n }\n\n const trackProperties = this.resolveTrackProperties(options.trackProperties, 'FETCH_OK');\n\n fetch.sendFetchOk();\n\n const fetchOk: FetchOk = {\n type: 'FETCH_OK',\n requestId,\n endOfTrack: options.endOfTrack ?? 0,\n endLocation: options.endLocation ?? { group: 0n, object: 0n },\n parameters: options.parameters ?? new Map(),\n trackProperties,\n };\n\n // §10.13: the response stream may have closed BEFORE this FETCH_OK. If so, both\n // are now done — reclaim the served fetch; otherwise keep it (reclaimServedFetch\n // reclaims when the data stream later closes).\n if (fetch.isPublisherComplete) this.incomingFetches.delete(requestId as bigint);\n\n return [this.sendControl(fetchOk)];\n }\n\n /**\n * Reject an incoming fetch request.\n * Sends REQUEST_ERROR and transitions the fetch to COMPLETED.\n * @see draft-ietf-moq-transport-16 §9.8\n */\n rejectFetch(requestId: bigint, errorCode: bigint, errorReason: string): SessionOutboundAction[] {\n const fetch = this.incomingFetches.get(requestId as bigint);\n if (!fetch) {\n throw new SessionError(\n `Unknown incoming fetch ${requestId}`,\n 'INVALID_STATE',\n );\n }\n\n fetch.sendRequestError(errorCode, errorReason);\n // REQUEST_ERROR is terminal — RECLAIM the incoming fetch state (bounded; a peer\n // cannot grow incomingFetches with valid unique rejected requests). The adapter\n // seals the request context, so its close callback will not run this cleanup.\n this.incomingFetches.delete(requestId as bigint);\n\n const requestError: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId,\n errorCode,\n retryInterval: varint(0n),\n errorReason,\n requestKind: 'FETCH', // draft-14 → FETCH_ERROR; ignored by 16/18\n };\n\n return [this.sendControl(requestError)];\n }\n\n /**\n * Get an incoming fetch (publisher-side) by request ID.\n */\n getIncomingFetch(requestId: bigint): FetchStateMachine | undefined {\n return this.incomingFetches.get(requestId as bigint);\n }\n\n // ─── Session Lifecycle ────────────────────────────────────────────────\n\n /**\n * Close the session.\n */\n close(error?: Varint, reason?: string): SessionOutboundAction[] {\n this._state = SessionState.CLOSED;\n this.fillRequests.clear();\n\n const closeAction: CloseConnectionAction = {\n type: 'close_connection',\n error: error ?? varint(0n),\n reason: reason ?? '',\n };\n\n return [closeAction];\n }\n\n /**\n * State-only transition to CLOSED for a TRANSPORT-level close (the underlying\n * WebTransport session ended remotely or failed). Unlike {@link close} this\n * emits NO close_connection action — there is no live transport to send a\n * CONNECTION_CLOSE capsule on. After this, the session refuses new requests\n * (subscribe/fetch/etc. assert ESTABLISHED), so nothing allocates state\n * against a dead transport. Idempotent.\n * @see draft-ietf-moq-transport-18 §3 (session termination)\n */\n handleTransportClosed(): void {\n this._state = SessionState.CLOSED;\n this.fillRequests.clear();\n }\n\n /**\n * Close the session with a specific error code.\n * Used internally for protocol violations.\n */\n private closeWithError(error: Varint, reason: string): SessionOutboundAction[] {\n this._state = SessionState.CLOSED;\n this.fillRequests.clear();\n\n return [{\n type: 'close_connection',\n error,\n reason,\n }];\n }\n\n // ─── Helpers ──────────────────────────────────────────────────────────\n\n private assertState(expected: SessionStateValue, operation: string): void {\n if (this._state !== expected) {\n throw new SessionError(\n `Cannot ${operation} in state ${this._state}; expected ${expected}`,\n 'INVALID_STATE',\n );\n }\n }\n\n private assertEstablishedOrDraining(operation: string): void {\n if (this._state !== SessionState.ESTABLISHED && this._state !== SessionState.DRAINING) {\n throw new SessionError(\n `Cannot ${operation} in state ${this._state}; session must be ESTABLISHED`,\n 'INVALID_STATE',\n );\n }\n }\n\n /**\n * §3.2.1 / §3.2.2: the Application MUST NOT publish tracks or namespaces whose\n * first Track Namespace field is reserved — exactly `.` (a single period) or\n * `.session` (session-level, managed by the implementation). Reject such local\n * API calls before any wire is emitted (cf. the invalid-filter local guard).\n * @throws {SessionError} for local misuse of a reserved namespace.\n */\n private assertNotReservedNamespace(namespace: Uint8Array[], operation: string): void {\n if (isReservedSessionNamespace(namespace)) {\n throw new SessionError(\n `Cannot ${operation} under the reserved .session namespace (§3.2.2)`,\n 'PROTOCOL_VIOLATION',\n );\n }\n if (isReservedDotNamespace(namespace)) {\n throw new SessionError(\n `Cannot ${operation} under the reserved '.' namespace (§3.2.1)`,\n 'PROTOCOL_VIOLATION',\n );\n }\n }\n\n /**\n * §3.2.1 / §3.2.2: a draft-18 inbound request for a track or namespace whose\n * Track Namespace first field is reserved — exactly `.` (§3.2.1) or `.session`\n * (§3.2.2, and we recognize no session-level tracks) — MUST be rejected\n * per-request with REQUEST_ERROR DOES_NOT_EXIST, NOT a session close. Other\n * `.`-prefixed namespaces are unrecognized reserved namespaces that §3.2.1\n * leaves application-visible, so they pass through here.\n *\n * Returns the reject actions (REQUEST_ERROR + replenish) when reserved — built\n * BEFORE any request state is created, so nothing is left dangling — otherwise\n * `undefined`. draft-14/16 are unaffected (§3.2 is a draft-18 rule).\n * @see draft-ietf-moq-transport-18 §3.2.1, §3.2.2\n */\n private rejectReservedNamespace(\n namespace: Uint8Array[],\n requestId: bigint,\n replenish: SessionOutboundAction[] | undefined,\n ): SessionOutboundAction[] | undefined {\n if (!isRequestStreamDraft(this._draftVersion)) return undefined;\n const isDot = isReservedDotNamespace(namespace);\n if (!isDot && !isReservedSessionNamespace(namespace)) return undefined;\n const errorMsg: RequestErrorMsg = {\n type: 'REQUEST_ERROR',\n requestId,\n errorCode: RequestErrorCode.DOES_NOT_EXIST,\n retryInterval: varint(0n),\n errorReason: isDot\n ? \"Reserved '.' namespace does not exist (§3.2.1)\"\n : 'Unrecognized .session namespace does not exist (§3.2.2)',\n };\n return [this.sendControl(errorMsg), ...(replenish ?? [])];\n }\n\n /**\n * Validate combined namespace (prefix + suffix) per §2.4.1.\n * \"Track Namespace is an ordered set of between 1 and 32 Track Namespace Fields\"\n * \"The length of a Track Namespace is the sum of the Track Namespace Field Length fields.\n * If an endpoint receives a Track Namespace...exceeding 4,096 bytes, it MUST close the\n * session with a PROTOCOL_VIOLATION.\"\n * @returns close_connection actions if invalid, undefined if valid\n */\n private validateCombinedNamespace(\n prefix: Uint8Array[],\n suffix: Uint8Array[],\n ): SessionOutboundAction[] | undefined {\n const combined = [...prefix, ...suffix];\n try {\n // §2.4.1: draft-18 permits a zero-field full namespace; draft-14/16 do not.\n validateTrackNamespace(combined, { allowEmpty: isRequestStreamDraft(this._draftVersion) });\n } catch (e) {\n return this.closeWithError(\n SessionErrorCode.PROTOCOL_VIOLATION,\n `Combined namespace (prefix=${prefix.length} + suffix=${suffix.length} = ${combined.length} fields) violates §2.4.1: ${(e as Error).message}`,\n );\n }\n return undefined;\n }\n\n private sendControl(message: ControlMessage): SendControlAction {\n return {\n type: 'send_control',\n message,\n };\n }\n\n /**\n * Check whether a known message parameter is valid for a control message type.\n *\n * Draft-14 includes GROUP_ORDER inline on PUBLISH; draft-16 does not.\n */\n private isParamValidForMessageType(key: bigint, messageType: string): boolean {\n if (\n this._draftVersion === 14 &&\n key === MessageParam.GROUP_ORDER &&\n messageType === 'PUBLISH'\n ) {\n return true;\n }\n\n const table = isDraft22(this._draftVersion)\n ? VALID_PARAMS_FOR_MESSAGE_TYPE_22\n : isRequestStreamDraft(this._draftVersion)\n ? VALID_PARAMS_FOR_MESSAGE_TYPE_18\n : VALID_PARAMS_FOR_MESSAGE_TYPE;\n return table.get(key as bigint)?.has(messageType) ?? false;\n }\n\n /** We advertise the default MAX_FILTER_RANGES=0; never silently ignore a filter. */\n private rejectUnsupportedRangeFilters(requestId: bigint, params: Parameters): SessionOutboundAction[] | undefined {\n if (!isDraft22(this._draftVersion)) return undefined;\n const nested = params.get(FILL_PARAMETERS)?.[0];\n const fill = nested instanceof Uint8Array ? decodeFillParameters(nested).parameters : undefined;\n if (![...RANGE_FILTER_PARAMS_22].some((type) => params.has(type) || fill?.has(type))) return undefined;\n return [this.sendControl({ type: 'REQUEST_ERROR', requestId, errorCode: varint(0x36n),\n retryInterval: varint(0n), errorReason: 'Range filters exceed MAX_FILTER_RANGES=0 (9.1.6)',\n })];\n }\n\n /** Validate parameter scope, multiplicity, and values using the selected draft. */\n private validateMessageParams(params: Parameters, messageType: string): { error: Varint; reason: string } | undefined {\n const isDraft18 = isRequestStreamDraft(this._draftVersion);\n const knownParams = isDraft22(this._draftVersion)\n ? KNOWN_MESSAGE_PARAMS_22\n : isDraft18 ? KNOWN_MESSAGE_PARAMS_18 : KNOWN_MESSAGE_PARAMS;\n for (const [key, values] of params) {\n // §9.2: Unknown message parameters are a protocol violation (draft-16/18).\n // Draft-14: ignore unknown params — different param sets between versions.\n if (!knownParams.has(key as bigint)) {\n if (this._draftVersion === 14) continue;\n return { error: SessionErrorCode.PROTOCOL_VIOLATION, reason: `Unknown message parameter type: ${key}` };\n }\n\n // Scope check. draft-18 §10.2.1: a KNOWN parameter out of scope MUST close\n // with PROTOCOL_VIOLATION. draft-14/16 §9.2.2: it MUST be ignored instead.\n if (!this.isParamValidForMessageType(key, messageType)) {\n if (isDraft18) {\n return {\n error: SessionErrorCode.PROTOCOL_VIOLATION,\n reason: `Parameter 0x${(key as bigint).toString(16)} is out of scope for ${messageType} (§10.2.1)`,\n };\n }\n continue; // draft-14/16: ignore parameters not defined for this message type\n }\n\n // §9.2.2.1: AUTHORIZATION_TOKEN may appear multiple times\n // All other known message parameters must be unique\n // draft-22 §3.3.2: Range Filters may repeat (one per SetID / Property Type).\n if (key !== MessageParam.AUTHORIZATION_TOKEN && values.length > 1\n && !(isDraft22(this._draftVersion) && RANGE_FILTER_PARAMS_22.has(key as bigint))) {\n return { error: SessionErrorCode.PROTOCOL_VIOLATION, reason: `Duplicate message parameter type: ${key}` };\n }\n\n // §9.2.2.1: Process AUTHORIZATION_TOKEN through alias cache + uniqueness check\n if (key === MessageParam.AUTHORIZATION_TOKEN) {\n const authError = this.processMessageAuthTokens(values);\n if (authError) return authError;\n continue;\n }\n\n // Value constraint validation for each parameter type\n for (const value of values) {\n const error = this.validateParamValue(key, value, messageType);\n if (error) return error;\n }\n }\n return undefined;\n }\n\n /** Validate a locally-authored binary FORWARD parameter before side effects. */\n private assertLocalForwardParameter(params: Parameters | undefined, context: string): void {\n const values = params?.get(MessageParam.FORWARD);\n if (values === undefined) return;\n if (values.length !== 1 || typeof values[0] !== 'bigint'\n || (values[0] !== 0n && values[0] !== 1n)) {\n throw new SessionError(\n `${context}: FORWARD must be exactly one value, 0 or 1`,\n 'INVALID_STATE',\n );\n }\n }\n\n /** Normalize a locally-authored Forward State before allocating request state. */\n private normalizeLocalForwardState(\n forward: unknown,\n context: string,\n ): ForwardStateValue | undefined {\n if (forward === undefined) return undefined;\n if (forward === false || forward === ForwardState.PAUSED) return ForwardState.PAUSED;\n if (forward === true || forward === ForwardState.ACTIVE) return ForwardState.ACTIVE;\n throw new SessionError(\n `${context}: Forward State must be 0 or 1`,\n 'INVALID_STATE',\n );\n }\n\n /**\n * Validate individual parameter value constraints.\n * Even-type parameters are varints — checked for value constraints (§9.2.2).\n * Odd-type parameters are bytes — checked for structural validity (§3.4).\n * @see draft-ietf-moq-transport-16 §9.2.2, §3.4\n * @returns Error with code and reason if validation fails, undefined if valid\n */\n private validateParamValue(key: bigint, value: ParameterValue, messageType: string): { error: Varint; reason: string } | undefined {\n // Even-type parameters: varint value constraint checks → PROTOCOL_VIOLATION\n if (typeof value === 'bigint') {\n switch (key) {\n case 0x35n:\n if (isDraft22(this._draftVersion) && value !== 0n && value !== 1n) {\n return { error: SessionErrorCode.PROTOCOL_VIOLATION, reason: `INCLUDE_PROPERTIES must be 0 or 1, got ${value}` };\n }\n break;\n // DELIVERY_TIMEOUT (0x02). draft-16 §9.2.2.2: MUST be > 0. draft-14 allows\n // 0; draft-18 §10.2.4 renames it OBJECT_DELIVERY_TIMEOUT where 0 means\n // \"no timeout\" (and SUBGROUP_DELIVERY_TIMEOUT 0x06 is unconstrained) — so\n // only draft-16 rejects 0.\n case MessageParam.DELIVERY_TIMEOUT:\n if (value === 0n && this._draftVersion === 16) {\n return { error: SessionErrorCode.PROTOCOL_VIOLATION, reason: 'DELIVERY_TIMEOUT must be greater than 0' };\n }\n break;\n\n // §9.2.2.8: FORWARD must be 0 or 1\n case MessageParam.FORWARD:\n if (value !== 0n && value !== 1n) {\n return { error: SessionErrorCode.PROTOCOL_VIOLATION, reason: `FORWARD must be 0 or 1, got ${value}` };\n }\n break;\n\n // §9.2.2.3: SUBSCRIBER_PRIORITY must be 0-255\n case MessageParam.SUBSCRIBER_PRIORITY:\n if (value < 0n || value > 255n) {\n return { error: SessionErrorCode.PROTOCOL_VIOLATION, reason: `SUBSCRIBER_PRIORITY must be 0-255, got ${value}` };\n }\n break;\n\n // §9.2.2.4: GROUP_ORDER validation is context-dependent in draft-14:\n // - SUBSCRIBE/FETCH: 0x0 = \"use publisher's order\" (valid), 0x1/0x2 valid, >0x2 error\n // - PUBLISH/PUBLISH_OK: 0x0 is a protocol error, only 0x1/0x2 valid\n // Draft-16: GROUP_ORDER is always 0x1 or 0x2 (no 0x0 anywhere)\n case MessageParam.GROUP_ORDER:\n if (this._draftVersion === 14 && (messageType === 'SUBSCRIBE' || messageType === 'FETCH')) {\n // Draft-14 §9.7/§9.16: 0x0 means \"use publisher's order\"\n if (value > 0x2n) {\n return { error: SessionErrorCode.PROTOCOL_VIOLATION, reason: `GROUP_ORDER must be 0x0-0x2, got ${value}` };\n }\n } else {\n if (value !== 0x1n && value !== 0x2n) {\n return { error: SessionErrorCode.PROTOCOL_VIOLATION, reason: `GROUP_ORDER must be Ascending (0x1) or Descending (0x2), got ${value}` };\n }\n }\n break;\n }\n return undefined;\n }\n\n // Non-varint parameter values. Draft-14/16 carry a byte string that is\n // validated structurally (§3.4 KEY_VALUE_FORMATTING_ERROR). Draft-18 carries\n // a typed Location for LARGEST_OBJECT, which is valid by construction.\n if (value instanceof Uint8Array) {\n switch (key) {\n case FILL_PARAMETERS:\n if (isDraft22(this._draftVersion)) {\n try { decodeFillParameters(value); }\n catch (err) {\n return { error: SessionErrorCode.PROTOCOL_VIOLATION, reason: `Invalid FILL_PARAMETERS: ${String(err)}` };\n }\n }\n break;\n // §9.2.2.7: LARGEST_OBJECT is a Location structure (two varints).\n case MessageParam.LARGEST_OBJECT:\n return this.validateLargestObject(value);\n\n // §9.2.2.5 / §5.1.2: SUBSCRIPTION_FILTER is a Subscription Filter structure.\n case MessageParam.SUBSCRIPTION_FILTER: {\n const reason = validateSubscriptionFilter(value, this._draftVersion);\n return reason ? { error: SessionErrorCode.PROTOCOL_VIOLATION, reason } : undefined;\n }\n }\n return undefined;\n }\n\n // Track Namespace tuple value (draft-18): only TRACK_NAMESPACE_PREFIX is a\n // namespace tuple. The prefix handler validates its shape (≤32 fields) and\n // overlap; here we only reject the tuple form on any other parameter.\n if (Array.isArray(value)) {\n if (key !== MessageParam.TRACK_NAMESPACE_PREFIX) {\n return {\n error: SessionErrorCode.KEY_VALUE_FORMATTING_ERROR,\n reason: `Parameter 0x${(key as bigint).toString(16)} must not be encoded as a Track Namespace tuple`,\n };\n }\n return undefined;\n }\n\n // Typed Location value (draft-18): only LARGEST_OBJECT is defined as a\n // Location. Any other parameter supplied as a Location is malformed.\n if (key !== MessageParam.LARGEST_OBJECT) {\n return {\n error: SessionErrorCode.KEY_VALUE_FORMATTING_ERROR,\n reason: `Parameter 0x${(key as bigint).toString(16)} must not be encoded as a Location`,\n };\n }\n return undefined; // LARGEST_OBJECT as a typed Location is valid by construction\n }\n\n /**\n * Validate LARGEST_OBJECT parameter bytes as a Location (§9.2.2.7, §1.4.1).\n * Must contain exactly two varints (group, object) with no trailing bytes.\n * @returns Error if malformed, undefined if valid\n */\n private validateLargestObject(bytes: Uint8Array): { error: Varint; reason: string } | undefined {\n try {\n const { bytesRead } = readLocation(bytes, 0);\n if (bytesRead !== bytes.length) {\n return {\n error: SessionErrorCode.KEY_VALUE_FORMATTING_ERROR,\n reason: `LARGEST_OBJECT has ${bytes.length - bytesRead} trailing bytes after Location`,\n };\n }\n } catch {\n return {\n error: SessionErrorCode.KEY_VALUE_FORMATTING_ERROR,\n reason: 'LARGEST_OBJECT is not a valid Location structure',\n };\n }\n return undefined;\n }\n\n /**\n * Validate message parameters for any control message that has them.\n * Setup messages are excluded (handled separately with different rules).\n * @returns Error with code and reason if validation fails, undefined if valid\n */\n private validateControlMessageParams(msg: ControlMessage): { error: Varint; reason: string } | undefined {\n // Messages with parameters field (excluding setup messages)\n if ('parameters' in msg && msg.parameters instanceof Map) {\n // Skip CLIENT_SETUP and SERVER_SETUP - they have setup parameters, not message parameters\n if (msg.type === 'CLIENT_SETUP' || msg.type === 'SERVER_SETUP') {\n return undefined;\n }\n // draft-18 §10.5: REQUEST_OK is a context-sensitive shorthand (PUBLISH_OK /\n // REQUEST_UPDATE_OK / TRACK_STATUS_OK / namespace responses). Its parameter\n // scope depends on WHICH request it answers, which is only known once the\n // request stream is resolved — so defer to handleRequestOk. (draft-14/16\n // REQUEST_OK keeps the by-type scope here.)\n if (isRequestStreamDraft(this._draftVersion) && msg.type === 'REQUEST_OK') {\n return undefined;\n }\n return this.validateMessageParams(msg.parameters, msg.type);\n }\n return undefined;\n }\n\n /**\n * Build a PUBLISH_OK parameter set from an inbound PUBLISH.\n *\n * PUBLISH and PUBLISH_OK do not share parameter semantics wholesale, so\n * only carry fields that intentionally define the initial subscription\n * state across both messages.\n */\n private buildPublishOkParamsFromPublish(params: Parameters): Parameters {\n const publishOkParams: Parameters = new Map();\n\n const forward = params.get(varint(MessageParam.FORWARD));\n if (forward && forward.length > 0) {\n publishOkParams.set(varint(MessageParam.FORWARD), [...forward]);\n }\n\n if (this._draftVersion === 14) {\n const groupOrder = params.get(varint(MessageParam.GROUP_ORDER));\n if (groupOrder && groupOrder.length > 0) {\n publishOkParams.set(varint(MessageParam.GROUP_ORDER), [...groupOrder]);\n } else {\n // Draft-14 encodes Group Order inline on PUBLISH_OK and forbids 0x0.\n publishOkParams.set(varint(MessageParam.GROUP_ORDER), [varint(1n)]);\n }\n }\n\n return publishOkParams;\n }\n}\n","/**\n * LOC (Low Overhead Container) type definitions.\n *\n * All types correspond to fields defined in draft-ietf-moq-loc-01 and\n * draft-ietf-moq-loc-04.\n * Each field is annotated with the spec section that defines it.\n *\n * @see draft-ietf-moq-loc-01\n * @module\n */\n\n// ─── Versions and property IDs ──────────────────────────────────────\n\n/** Supported LOC draft dialects. */\nexport type LocVersion = 1 | 4;\n\n/**\n * LOC-01 property IDs. Even IDs carry integer values; odd IDs carry\n * length-prefixed bytes.\n *\n * @see draft-ietf-moq-loc-01 §2.3\n */\nexport const Loc01PropertyId = {\n /** Wall-clock capture time in microseconds. @see loc-01 §2.3.1.1 */\n CAPTURE_TIMESTAMP: 0x02,\n /** RFC 9626 video frame marking as an integer. @see loc-01 §2.3.2.2 */\n VIDEO_FRAME_MARKING: 0x04,\n /** RFC 6464 audio level + voice activity. @see loc-01 §2.3.3.1 */\n AUDIO_LEVEL: 0x06,\n /** Video codec configuration extradata. @see loc-01 §2.3.2.1 */\n VIDEO_CONFIG: 0x0d,\n} as const;\n\n/**\n * LOC-04 property IDs, as registered in the MOQ Properties registry.\n *\n * @see draft-ietf-moq-loc-04 §6.1\n */\nexport const Loc04PropertyId = {\n /** Timestamp units per second. Absent means microseconds. @see loc-04 §2.3.1.2 */\n TIMESCALE: 0x08,\n /** RFC 9626 video frame marking as 1 to 4 bytes. @see loc-04 §2.3.2.2 */\n VIDEO_FRAME_MARKING: 0x09,\n /** RFC 6464 audio level + voice activity. @see loc-04 §2.3.3.2 */\n AUDIO_LEVEL: 0x0c,\n /** Video codec configuration extradata. @see loc-04 §2.3.2.1 */\n VIDEO_CONFIG: 0x0d,\n /** Audio codec configuration. @see loc-04 §2.3.3.1 */\n AUDIO_CONFIG: 0x0f,\n /** Frame timestamp in Timescale units. @see loc-04 §2.3.1.1 */\n TIMESTAMP: 0x10,\n} as const;\n\n/** @deprecated Use {@link Loc01PropertyId}. Kept so existing imports compile. */\nexport const LocExtensionId = Loc01PropertyId;\n\n// ─── Video Frame Marking ────────────────────────────────────────────\n\n/**\n * Parsed RFC 9626 video frame marking flags.\n *\n * Encoded as a varint in LOC extension ID 4. The least significant bits\n * carry the RFC 9626 header fields.\n *\n * First byte (always present):\n * ```\n * Bit 7: S — Start of frame\n * Bit 6: E — End of frame\n * Bit 5: I — Independent frame (keyframe)\n * Bit 4: D — Discardable frame\n * Bit 3: B — Base layer sync (independent of second-byte presence)\n * Bits 2-0: TID — Temporal layer ID (0-7)\n * ```\n *\n * Second byte (present when varint value >= 256):\n * ```\n * Bits 7-0: LID — Layer ID (0-255)\n * ```\n *\n * @see draft-ietf-moq-loc-01 §2.3.2.2\n * @see RFC 9626 §3.1\n */\nexport interface VideoFrameMarking {\n /** Start of frame. @see RFC 9626 §3.1 */\n readonly startOfFrame: boolean;\n /** End of frame. @see RFC 9626 §3.1 */\n readonly endOfFrame: boolean;\n /** Independent frame (keyframe). @see RFC 9626 §3.1 */\n readonly independent: boolean;\n /** Discardable frame. @see RFC 9626 §3.1 */\n readonly discardable: boolean;\n /** Base layer sync. MUST be 0 when TID is 0. @see RFC 9626 §3.1 */\n readonly baseLayerSync: boolean;\n /** Temporal layer ID (0-7). @see RFC 9626 §3.1 */\n readonly temporalId: number;\n /**\n * Layer ID (0-255). Present when the LOC-01 integer form carries a second\n * byte (varint value >= 256) or the LOC-04 byte form has at least 2 bytes.\n * @see RFC 9626 §3.1\n */\n readonly layerId?: number;\n /**\n * TL0PICIDX (0-255). Present only when the LOC-04 byte form carries the\n * RFC 9626 long header. Never set by the LOC-01 integer form.\n * @see RFC 9626 §3.1\n */\n readonly tl0PicIdx?: number;\n}\n\n// ─── Audio Level ────────────────────────────────────────────────────\n\n/**\n * Parsed RFC 6464 audio level indication.\n *\n * Encoded as a varint in LOC extension ID 6. The least significant 8 bits\n * carry the RFC 6464 header field.\n *\n * ```\n * Bit 7: V — Voice activity (1 = speech detected)\n * Bits 6-0: level — Audio magnitude in -dBov (0 = loudest, 127 = silence)\n * ```\n *\n * @see draft-ietf-moq-loc-01 §2.3.3.1\n * @see RFC 6464 §3\n */\nexport interface AudioLevel {\n /** Voice activity detected. @see RFC 6464 §3 */\n readonly voiceActivity: boolean;\n /** Audio magnitude in -dBov (0 = loudest, 127 = silence). @see RFC 6464 §3 */\n readonly level: number;\n}\n\n// ─── Parsed Headers ─────────────────────────────────────────────────\n\n/** Value of an unknown extension: varint (even ID) or bytes (odd ID). */\nexport type LocExtensionValue = bigint | Uint8Array;\n\n/**\n * Parsed LOC header extensions from a MOQ Object.\n *\n * Contains structured data extracted from the opaque `extensions: Uint8Array`\n * on `MoqtObjectData`. Unknown extension IDs are preserved in the `unknown` map.\n *\n * @see draft-ietf-moq-loc-01 §2.3\n */\nexport interface LocHeaders {\n /**\n * Presentation time in microseconds. Under LOC-01 this is the wall-clock\n * CaptureTimestamp. Under LOC-04 it is derived from Timestamp and\n * Timescale (`timestamp * 1e6 / timescale`, truncating); without a\n * Timescale it is the Timestamp itself. See {@link timestampIsWallClock}.\n * @see draft-ietf-moq-loc-01 §2.3.1.1\n * @see draft-ietf-moq-loc-04 §2.3.1.1, §2.3.1.2\n */\n readonly captureTimestamp?: bigint;\n /**\n * Sample duration in microseconds, supplied by a container frame interface.\n * Local decode metadata only; not a LOC wire property.\n */\n readonly duration?: number;\n /**\n * Video frame marking flags (RFC 9626).\n * @see draft-ietf-moq-loc-01 §2.3.2.2\n */\n readonly videoFrameMarking?: VideoFrameMarking;\n /**\n * Audio level indication (RFC 6464).\n * @see draft-ietf-moq-loc-01 §2.3.3.1\n */\n readonly audioLevel?: AudioLevel;\n /**\n * Video codec configuration \"extradata\" bytes.\n * Maps to WebCodecs `VideoDecoderConfig.description`.\n * @see draft-ietf-moq-loc-01 §2.3.2.1\n */\n readonly videoConfig?: Uint8Array;\n /**\n * Audio codec configuration bytes. Maps to WebCodecs\n * `AudioDecoderConfig.description`. LOC-04 only.\n * @see draft-ietf-moq-loc-04 §2.3.3.1\n */\n readonly audioConfig?: Uint8Array;\n /**\n * Raw Timestamp value as carried on the wire, in {@link timescale} units.\n * Under LOC-01 this equals {@link captureTimestamp}.\n * @see draft-ietf-moq-loc-04 §2.3.1.1\n */\n readonly timestamp?: bigint;\n /**\n * Timestamp units per second. Never set by the LOC-01 path. When absent\n * the timestamp is microseconds since the Unix epoch.\n * @see draft-ietf-moq-loc-04 §2.3.1.2\n */\n readonly timescale?: bigint;\n /**\n * True when a timestamp was present and no Timescale was seen, so the\n * value is wall-clock microseconds since the Unix epoch. False when a\n * Timescale made it media time. Undefined when there is no timestamp.\n */\n readonly timestampIsWallClock?: boolean;\n /**\n * Dialect detected on parse: 4 if any LOC-04-only id was present, 1 if\n * only LOC-01 ids were present, undefined otherwise. Ignored on encode.\n */\n readonly version?: LocVersion;\n /**\n * Unknown extension headers, keyed by absolute extension ID.\n *\n * Keys are `bigint` so a full-width (up to 2^64-1) property ID is preserved\n * losslessly — a draft-18 property block can carry IDs above `Number`'s safe\n * range, and narrowing them would silently collide distinct extensions.\n * Even IDs map to bigint (varint/vi64 value), odd IDs to Uint8Array.\n */\n readonly unknown?: ReadonlyMap<bigint, LocExtensionValue>;\n}\n\n// ─── WebCodecs-compatible chunk init ─────────────────────────────────\n\n/**\n * Exact supplied source timestamp before conversion to a WebCodecs number.\n * Retains raw ticks when available, otherwise supplied normalized microseconds.\n * Media time has an application-defined origin; unknown means no domain was\n * declared. This is decode-input metadata, not evidence of a rendered frame.\n * @see draft-ietf-moq-loc-01 §2.3.1.1\n * @see draft-ietf-moq-loc-04 §2.3.1.1, §2.3.1.2\n */\nexport interface SourceTimestamp {\n readonly ticks: bigint;\n readonly ticksPerSecond: bigint;\n readonly domain: 'unix' | 'media' | 'unknown';\n}\n\n/**\n * Initialization data for creating an `EncodedVideoChunk`.\n *\n * Pure TypeScript type compatible with the WebCodecs `EncodedVideoChunkInit`\n * interface, usable without browser APIs.\n *\n * @see draft-ietf-moq-loc-01 §2.1\n * @see https://www.w3.org/TR/webcodecs/#encodedvideochunk-interface\n */\nexport interface VideoChunkInit {\n /** \"key\" for independent frames, \"delta\" for dependent frames. */\n readonly type: 'key' | 'delta';\n /** Timestamp in microseconds. From CaptureTimestamp if available. */\n readonly timestamp: number;\n /** Immutable source time; absent when time or consistent units are unavailable. */\n readonly sourceTimestamp?: SourceTimestamp;\n /** Duration in microseconds (optional). */\n readonly duration?: number;\n /** Raw codec bitstream (LOC payload = EncodedVideoChunk internal data). */\n readonly data: Uint8Array;\n}\n\n/**\n * Initialization data for creating an `EncodedAudioChunk`.\n *\n * Pure TypeScript type compatible with the WebCodecs `EncodedAudioChunkInit`\n * interface, usable without browser APIs.\n *\n * @see draft-ietf-moq-loc-01 §2\n * @see https://www.w3.org/TR/webcodecs/#encodedaudiochunk-interface\n */\nexport interface AudioChunkInit {\n /** Audio chunks are always \"key\" (each chunk is independently decodable). */\n readonly type: 'key';\n /** Timestamp in microseconds. From CaptureTimestamp if available. */\n readonly timestamp: number;\n /** Immutable source time; absent when time or consistent units are unavailable. */\n readonly sourceTimestamp?: SourceTimestamp;\n /** Duration in microseconds (optional). */\n readonly duration?: number;\n /** Raw codec bitstream (LOC payload = EncodedAudioChunk internal data). */\n readonly data: Uint8Array;\n}\n\n// ─── Track context ──────────────────────────────────────────────────\n\n/**\n * Track-scoped defaults for properties that draft-04 allows at Track scope.\n * Object-level values always win; these fill only what the object omitted.\n *\n * @see draft-ietf-moq-loc-04 §6.1 (Scope column)\n */\nexport interface LocTrackContext {\n readonly timescale?: bigint;\n readonly videoConfig?: Uint8Array;\n readonly audioConfig?: Uint8Array;\n}\n","/**\n * LOC Layer B — semantic resolution.\n *\n * The property WIRE (bytes ⇄ ordered {@link PropertyMap}) is Layer A, owned by\n * `@openmoq/transport`. This module is Layer B: it interprets a decoded, ordered\n * `PropertyMap` as LOC metadata, and projects structured {@link LocHeaders} back\n * to a `PropertyMap` for encoding. There is NO byte-level parsing here.\n *\n * The Layer A/B split lets the same LOC semantics run over any transport wire\n * profile (draft-14 absolute, draft-16 delta QUIC-varint, draft-18 delta vi64):\n * only Layer A changes per profile; the interpretation below does not.\n *\n * Duplicate known IDs resolve last-wins (a Layer B policy — Layer A preserves the\n * raw duplicates); unknown IDs are preserved losslessly with their full-width\n * `bigint` id, never narrowed.\n *\n * @see draft-ietf-moq-loc-01 §2.3\n * @see draft-ietf-moq-loc-04 §2.3\n * @module\n */\n\nimport type { PropertyEntry, PropertyMap } from '@openmoq/transport';\nimport { Loc01PropertyId, Loc04PropertyId } from './types.js';\nimport type { LocHeaders, LocExtensionValue, LocTrackContext, LocVersion, VideoFrameMarking, AudioLevel } from './types.js';\nimport { parseVideoFrameMarking, encodeVideoFrameMarking, parseVideoFrameMarkingBytes, encodeVideoFrameMarkingBytes } from './video.js';\nimport { parseAudioLevel, encodeAudioLevel } from './audio.js';\nimport { LocHeaderError, LocEncodeError } from './errors.js';\n\nconst ID01 = {\n captureTimestamp: BigInt(Loc01PropertyId.CAPTURE_TIMESTAMP),\n videoFrameMarking: BigInt(Loc01PropertyId.VIDEO_FRAME_MARKING),\n audioLevel: BigInt(Loc01PropertyId.AUDIO_LEVEL),\n videoConfig: BigInt(Loc01PropertyId.VIDEO_CONFIG),\n} as const;\n\nconst ID04 = {\n timescale: BigInt(Loc04PropertyId.TIMESCALE),\n videoFrameMarking: BigInt(Loc04PropertyId.VIDEO_FRAME_MARKING),\n audioLevel: BigInt(Loc04PropertyId.AUDIO_LEVEL),\n videoConfig: BigInt(Loc04PropertyId.VIDEO_CONFIG),\n audioConfig: BigInt(Loc04PropertyId.AUDIO_CONFIG),\n timestamp: BigInt(Loc04PropertyId.TIMESTAMP),\n} as const;\n\nconst MICROS_PER_SECOND = 1_000_000n;\n\ntype Mutable = { -readonly [K in keyof LocHeaders]: LocHeaders[K] };\n\n/**\n * Interpret an ordered {@link PropertyMap} as LOC headers from either draft.\n *\n * One pass collects LOC-01 and LOC-04 candidates separately. Any LOC-04-only id\n * makes the block version 4, and LOC-01 candidates then go to `unknown`, since\n * they no longer identify those fields in LOC-04 (0x02 is a Secure Objects\n * KeyID, for example). Only LOC-01 ids means\n * version 1. Duplicate known ids resolve last-wins. Track context fills only\n * fields the object omitted. `captureTimestamp` is always microseconds.\n *\n * @see draft-ietf-moq-loc-01 §2.3\n * @see draft-ietf-moq-loc-04 §2.3, §6.1\n */\nexport function resolveLocHeaders(propertyMap: PropertyMap, track?: LocTrackContext): LocHeaders {\n // LOC-01 candidates\n let ts01: bigint | undefined;\n let vfm01: bigint | undefined;\n let al01: bigint | undefined;\n // LOC-04 candidates\n let ts04: bigint | undefined;\n let scale04: bigint | undefined;\n let vfm04: Uint8Array | undefined;\n let al04: bigint | undefined;\n let audioConfig: Uint8Array | undefined;\n // Shared\n let videoConfig: Uint8Array | undefined;\n let unknown: Map<bigint, LocExtensionValue> | undefined;\n let saw01 = false;\n let saw04 = false;\n\n for (const { id, value } of propertyMap) {\n if (typeof value === 'bigint') {\n switch (id) {\n case ID01.captureTimestamp: ts01 = value; saw01 = true; break;\n case ID01.videoFrameMarking: vfm01 = value; saw01 = true; break;\n case ID01.audioLevel: al01 = value; saw01 = true; break;\n case ID04.timestamp: ts04 = value; saw04 = true; break;\n case ID04.timescale: scale04 = value; saw04 = true; break;\n case ID04.audioLevel: al04 = value; saw04 = true; break;\n default: (unknown ??= new Map()).set(id, value); break;\n }\n } else {\n switch (id) {\n case ID04.videoConfig: videoConfig = value; break;\n case ID04.videoFrameMarking: vfm04 = value; saw04 = true; break;\n case ID04.audioConfig: audioConfig = value; saw04 = true; break;\n default: (unknown ??= new Map()).set(id, value); break;\n }\n }\n }\n\n const out: Mutable = {};\n let version: LocVersion | undefined;\n let timestamp: bigint | undefined;\n let timescale: bigint | undefined;\n let videoFrameMarking: VideoFrameMarking | undefined;\n let audioLevel: AudioLevel | undefined;\n\n if (saw04) {\n version = 4;\n if (saw01) {\n if (ts01 !== undefined) (unknown ??= new Map()).set(ID01.captureTimestamp, ts01);\n if (vfm01 !== undefined) (unknown ??= new Map()).set(ID01.videoFrameMarking, vfm01);\n if (al01 !== undefined) (unknown ??= new Map()).set(ID01.audioLevel, al01);\n }\n timestamp = ts04;\n timescale = scale04 ?? track?.timescale;\n if (vfm04 !== undefined) videoFrameMarking = parseVideoFrameMarkingBytes(vfm04);\n if (al04 !== undefined) audioLevel = parseAudioLevel(al04);\n } else if (saw01) {\n version = 1;\n timestamp = ts01;\n if (vfm01 !== undefined) videoFrameMarking = parseVideoFrameMarking(vfm01);\n if (al01 !== undefined) audioLevel = parseAudioLevel(al01);\n } else {\n timescale = track?.timescale;\n }\n\n videoConfig ??= track?.videoConfig;\n audioConfig ??= track?.audioConfig;\n\n if (version !== undefined) out.version = version;\n if (timestamp !== undefined) {\n out.timestamp = timestamp;\n if (timescale !== undefined) {\n if (timescale <= 0n) throw new LocHeaderError('timescale', 'must be positive');\n out.timescale = timescale;\n out.captureTimestamp = (timestamp * MICROS_PER_SECOND) / timescale;\n out.timestampIsWallClock = false;\n } else {\n out.captureTimestamp = timestamp;\n out.timestampIsWallClock = true;\n }\n } else if (timescale !== undefined) {\n if (timescale <= 0n) throw new LocHeaderError('timescale', 'must be positive');\n out.timescale = timescale;\n }\n if (videoFrameMarking !== undefined) out.videoFrameMarking = videoFrameMarking;\n if (audioLevel !== undefined) out.audioLevel = audioLevel;\n if (videoConfig !== undefined) out.videoConfig = videoConfig;\n if (audioConfig !== undefined) out.audioConfig = audioConfig;\n if (unknown !== undefined) out.unknown = unknown;\n return out;\n}\n\n/**\n * Project structured {@link LocHeaders} to an ordered {@link PropertyMap} for\n * the given LOC version. Layer A canonicalises the order, so entry order here\n * is not significant.\n *\n * Version 4: `timestamp` (else `captureTimestamp`) into 0x10; `timescale` into\n * 0x08 only when `timestamp` was the source. Version 1: wall-clock microseconds\n * into 0x02. Media time cannot be converted to wall-clock time without an anchor.\n * Fields that version 1 cannot carry throw {@link LocEncodeError}.\n *\n * @see draft-ietf-moq-loc-01 §2.3\n * @see draft-ietf-moq-loc-04 §2.3\n */\nexport function locHeadersToPropertyMap(headers: LocHeaders, version: LocVersion = 1): PropertyEntry[] {\n if (version !== 1 && version !== 4) {\n throw new LocEncodeError('locVersion', 'must be 1 or 4');\n }\n if (headers.timescale !== undefined && headers.timescale <= 0n) {\n throw new LocEncodeError('timescale', 'must be positive');\n }\n if (version === 1 && (headers.timescale !== undefined || headers.timestampIsWallClock === false)) {\n throw new LocEncodeError('timescale', 'media time is not representable as LOC-01 wall-clock time');\n }\n if (headers.timestampIsWallClock === false && headers.timescale === undefined) {\n throw new LocEncodeError('timescale', 'required for media time');\n }\n const entries: PropertyEntry[] = [];\n const emitted = new Set<bigint>();\n const emit = (id: bigint, value: LocExtensionValue) => {\n entries.push({ id, value });\n emitted.add(id);\n };\n\n if (version === 4) {\n if (headers.timestamp !== undefined) {\n emit(ID04.timestamp, headers.timestamp);\n if (headers.timescale !== undefined) emit(ID04.timescale, headers.timescale);\n } else if (headers.captureTimestamp !== undefined) {\n if (headers.timescale !== undefined) {\n throw new LocEncodeError('timescale', 'requires timestamp; captureTimestamp is always microseconds');\n }\n emit(ID04.timestamp, headers.captureTimestamp);\n } else if (headers.timescale !== undefined) {\n emit(ID04.timescale, headers.timescale);\n }\n if (headers.videoFrameMarking !== undefined) {\n emit(ID04.videoFrameMarking, encodeVideoFrameMarkingBytes(headers.videoFrameMarking));\n }\n if (headers.audioLevel !== undefined) {\n emit(ID04.audioLevel, encodeAudioLevel(headers.audioLevel));\n }\n if (headers.videoConfig !== undefined) emit(ID04.videoConfig, headers.videoConfig);\n if (headers.audioConfig !== undefined) emit(ID04.audioConfig, headers.audioConfig);\n } else {\n const micros = headers.captureTimestamp ?? headers.timestamp;\n if (micros !== undefined) emit(ID01.captureTimestamp, micros);\n if (headers.videoFrameMarking !== undefined) {\n if (headers.videoFrameMarking.tl0PicIdx !== undefined) {\n throw new LocEncodeError('videoFrameMarking.tl0PicIdx', 'not representable in LOC-01');\n }\n emit(ID01.videoFrameMarking, encodeVideoFrameMarking(headers.videoFrameMarking));\n }\n if (headers.audioLevel !== undefined) {\n emit(ID01.audioLevel, encodeAudioLevel(headers.audioLevel));\n }\n if (headers.videoConfig !== undefined) emit(ID01.videoConfig, headers.videoConfig);\n if (headers.audioConfig !== undefined) {\n throw new LocEncodeError('audioConfig', 'not representable in LOC-01');\n }\n }\n\n if (headers.unknown) {\n for (const [id, value] of headers.unknown) {\n if (emitted.has(id)) continue;\n entries.push({ id, value });\n }\n }\n return entries;\n}\n","/**\n * LOC header extension parsing and encoding.\n *\n * Bridges the opaque `extensions: Uint8Array` from MOQ Object Header Extensions\n * and structured {@link LocHeaders}. This module is the compatibility layer over\n * the two-layer split:\n * - Layer A ({@link @openmoq/transport}) — the profile-aware property WIRE codec\n * (bytes ⇄ ordered `PropertyMap`), shared across draft-14/16/18.\n * - Layer B ({@link ./property-map.js}) — LOC semantic resolution (PropertyMap\n * ⇄ `LocHeaders`).\n *\n * The wire profile is selected explicitly: `deltaEncoded: false` → draft-14\n * absolute QUIC-varint; the default → draft-16 delta QUIC-varint; an explicit\n * `wireProfile` → any of the three (draft-18 carries a **vi64** block, whose\n * integers diverge from the QUIC varint at value 64, so it needs its own\n * profile — `deltaEncoded` alone cannot express it).\n *\n * @see draft-ietf-moq-loc-01 §2.3\n * @see draft-ietf-moq-loc-04 §2.3\n * @see draft-ietf-moq-transport-16 §2.5 (Extension Headers)\n * @module\n */\n\nimport { decodePropertyBlock, encodePropertyBlock, type PropertyWireProfile } from '@openmoq/transport';\nimport type {\n LocHeaders,\n LocTrackContext,\n LocVersion,\n VideoChunkInit,\n AudioChunkInit,\n SourceTimestamp,\n} from './types.js';\nimport { resolveLocHeaders, locHeadersToPropertyMap } from './property-map.js';\n\n/**\n * Options for LOC header parsing/encoding.\n */\nexport interface LocHeaderOptions {\n /**\n * Whether type IDs are delta-encoded (draft-16) or absolute (draft-14).\n *\n * Draft-16 §1.4.2: \"Key-Value-Pairs encode a Type value as a delta from\n * the previous Type value, or from 0 if there is no previous Type value.\"\n *\n * Draft-14 §1.4.2: \"Type: an unsigned integer, encoded as a varint,\n * identifying the type of the value.\"\n *\n * Default: true (draft-16 delta encoding). Ignored when {@link wireProfile}\n * is set.\n *\n * @see draft-ietf-moq-transport-16 §1.4.2\n * @see draft-ietf-moq-transport-14 §1.4.2\n */\n readonly deltaEncoded?: boolean;\n\n /**\n * Explicit transport wire profile. Overrides {@link deltaEncoded}. Draft-18\n * (`d18-delta-vi64`) MUST be selected here — it uses a vi64 integer codec\n * that diverges from the QUIC varint at value 64, so `deltaEncoded: true`\n * (which stays on the draft-16 QUIC-varint profile) would mis-encode and\n * mis-decode every value ≥ 64.\n *\n * @see draft-ietf-moq-transport-18 §1.4.1 (vi64), §1.4.3 (property block)\n */\n readonly wireProfile?: PropertyWireProfile;\n\n /**\n * LOC draft to emit. Encode only; parse auto-detects. Default 1.\n * Version 1 cannot carry Timescale, Audio Config, or TL0PICIDX and\n * throws {@link LocEncodeError} for them.\n *\n * @see draft-ietf-moq-loc-04 §6.1\n */\n readonly locVersion?: LocVersion;\n\n /**\n * Track-scoped defaults (Timescale, Video Config, Audio Config). Parse\n * only. Object-level values always win.\n *\n * @see draft-ietf-moq-loc-04 §6.1 (Scope column)\n */\n readonly track?: LocTrackContext;\n}\n\n/** Map LOC options to a transport wire profile, preserving legacy defaults. */\nfunction resolveWireProfile(options?: LocHeaderOptions): PropertyWireProfile {\n if (options?.wireProfile) return options.wireProfile;\n return options?.deltaEncoded === false ? 'd14-absolute-varint' : 'd16-delta-varint';\n}\n\n/** The transport wire profile for a negotiated MoQT draft version. */\nexport function locWireProfileForDraft(draft: number): PropertyWireProfile {\n switch (draft) {\n case 14:\n return 'd14-absolute-varint';\n case 18:\n case 22:\n return 'd18-delta-vi64';\n default:\n return 'd16-delta-varint';\n }\n}\n\n/**\n * Parse LOC header extensions from raw MOQ Object extension bytes.\n *\n * Decodes the property block (Layer A) then resolves LOC semantics (Layer B).\n * Known LOC extension IDs become structured fields; unknown IDs are preserved in\n * `unknown` under their full-width `bigint` id.\n *\n * Type IDs are delta-encoded by default (draft-16 §1.4.2). Pass\n * `{ deltaEncoded: false }` for draft-14 absolute type IDs, or\n * `{ wireProfile: 'd18-delta-vi64' }` for draft-18.\n *\n * Accepts LOC-01 and LOC-04 property sets; see `LocHeaders.version`.\n *\n * @param extensions Raw extension bytes from `MoqtObjectData.extensions`\n * @param options Parsing options (defaults to draft-16 delta)\n * @returns Parsed LOC headers\n * @see draft-ietf-moq-loc-01 §2.3\n * @see draft-ietf-moq-loc-04 §2.3\n */\nexport function parseLocHeaders(\n extensions: Uint8Array | undefined,\n options?: LocHeaderOptions,\n): LocHeaders {\n if (!extensions || extensions.length === 0) {\n return options?.track ? resolveLocHeaders([], options.track) : {};\n }\n const { entries } = decodePropertyBlock(extensions, 0, { profile: resolveWireProfile(options) });\n return resolveLocHeaders(entries, options?.track);\n}\n\n/**\n * Encode LOC headers into raw MOQ Object extension bytes.\n *\n * Projects to a PropertyMap (Layer B) then encodes canonically (Layer A):\n * stable ascending-ID order, minimal integer encodings. Returns undefined if no\n * headers are present.\n *\n * Preserves LOC-01 output by default. Pass `locVersion: 4` for LOC-04.\n *\n * @param headers Structured LOC headers\n * @param options Encoding options (defaults to draft-16 delta)\n * @returns Encoded extension bytes, or undefined if empty\n * @see draft-ietf-moq-loc-01 §2.3\n * @see draft-ietf-moq-loc-04 §2.3\n */\nexport function encodeLocHeaders(\n headers: LocHeaders,\n options?: LocHeaderOptions,\n): Uint8Array | undefined {\n const entries = locHeadersToPropertyMap(headers, options?.locVersion ?? 1);\n if (entries.length === 0) return undefined;\n return encodePropertyBlock(entries, resolveWireProfile(options));\n}\n\nfunction sourceTimestamp(headers: LocHeaders): SourceTimestamp | undefined {\n const micros = headers.captureTimestamp;\n if (micros === undefined) return undefined;\n const scale = headers.timescale;\n if (scale !== undefined && (scale <= 0n || headers.timestampIsWallClock === true)) return undefined;\n\n if (headers.timestamp !== undefined) {\n if (scale === undefined && headers.timestampIsWallClock === false) return undefined;\n const ticksPerSecond = scale ?? 1_000_000n;\n // LOC truncates to microseconds; the LOCMAF frame interface rounds.\n const scaled = headers.timestamp * 1_000_000n;\n const truncated = scaled / ticksPerSecond;\n const rounded = (scaled + (scaled >= 0n ? ticksPerSecond / 2n : -(ticksPerSecond / 2n))) / ticksPerSecond;\n if (micros !== truncated && micros !== rounded) return undefined;\n return Object.freeze({ ticks: headers.timestamp, ticksPerSecond, domain: scale === undefined ? 'unix' : 'media' });\n }\n\n return Object.freeze({\n ticks: micros,\n ticksPerSecond: 1_000_000n,\n domain: headers.timestampIsWallClock === true ? 'unix'\n : headers.timestampIsWallClock === false || scale !== undefined ? 'media' : 'unknown',\n });\n}\n\n/**\n * Create a WebCodecs-compatible `EncodedVideoChunkInit` from LOC payload + headers.\n *\n * - `type`: \"key\" if VideoFrameMarking.independent is true, \"delta\" otherwise\n * - `timestamp`: from CaptureTimestamp (microseconds), or 0 if absent\n * - `sourceTimestamp`: exact supplied time and domain, when available\n * - `data`: the LOC payload (zero-copy reference)\n *\n * @param payload LOC payload (= MoqtObjectData.payload)\n * @param headers Parsed LOC headers\n * @returns VideoChunkInit ready for `new EncodedVideoChunk(init)`\n * @see draft-ietf-moq-loc-01 §2.1, §2.2, §2.3.1.1 (CaptureTimestamp), §2.3.2.2 (VideoFrameMarking)\n */\nexport function toVideoChunkInit(\n payload: Uint8Array,\n headers: LocHeaders,\n): VideoChunkInit {\n const isKey = headers.videoFrameMarking?.independent === true;\n const timestamp = headers.captureTimestamp !== undefined\n ? Number(headers.captureTimestamp)\n : 0;\n const source = sourceTimestamp(headers);\n\n return {\n type: isKey ? 'key' : 'delta',\n timestamp,\n ...(source !== undefined ? { sourceTimestamp: source } : {}),\n ...(headers.duration !== undefined ? { duration: headers.duration } : {}),\n data: payload,\n };\n}\n\n/**\n * Create a WebCodecs-compatible `EncodedAudioChunkInit` from LOC payload + headers.\n *\n * Audio chunks are always \"key\" type — each encoded audio chunk is\n * independently decodable (Opus, AAC-LC frames are self-contained).\n *\n * @param payload LOC payload (= MoqtObjectData.payload)\n * @param headers Parsed LOC headers\n * @returns AudioChunkInit ready for `new EncodedAudioChunk(init)`\n * @see draft-ietf-moq-loc-01 §2 (payload format), §2.3.1.1 (CaptureTimestamp)\n */\nexport function toAudioChunkInit(\n payload: Uint8Array,\n headers: LocHeaders,\n): AudioChunkInit {\n const timestamp = headers.captureTimestamp !== undefined\n ? Number(headers.captureTimestamp)\n : 0;\n const source = sourceTimestamp(headers);\n\n return {\n type: 'key',\n timestamp,\n ...(source !== undefined ? { sourceTimestamp: source } : {}),\n ...(headers.duration !== undefined ? { duration: headers.duration } : {}),\n data: payload,\n };\n}\n","/**\n * Playback pipeline orchestrator.\n *\n * Wires jitter-buffer + gap-detector + decoder-state + sync together.\n * Accepts MoqtObject + LocHeaders, emits DecoderCommand + PlaybackEvent.\n *\n * Sans-I/O: no WebCodecs, no Canvas, no AudioContext — just pure logic.\n * The browser adapter consumes the emitted DecoderCommands.\n *\n * @see draft-ietf-moq-loc-01 §4.2 (decode order)\n * @see draft-ietf-moq-transport-16 §10.2.1.1 (Object Status)\n * @module\n */\n\nimport type { MoqtObject, MoqtObjectData } from '@openmoq/transport';\nimport { ObjectStatus } from '@openmoq/transport';\nimport type { LocHeaders, VideoFrameMarking } from '@openmoq/loc';\nimport { toVideoChunkInit, toAudioChunkInit } from '@openmoq/loc';\n\nimport { isKeyframePayload } from './keyframe-validator.js';\nimport { JitterBuffer } from './jitter-buffer.js';\nimport { GapDetector, GapAction } from './gap-detector.js';\nimport { DecoderStateMachine } from './decoder-state.js';\nimport { SyncController } from './sync.js';\nimport { AdaptiveToleranceController, DEFAULT_TOLERANCE_CONFIG } from './adaptive-tolerance.js';\nimport type { RecoveryController } from './recovery.js';\nimport type { ClockSource, DecoderCommand, PlaybackEvent, PlaybackConfig, DecoderFeedback } from './types.js';\n\n// ─── Pipeline ────────────────────────────────────────────────────────\n\n/**\n * Pipeline constructor options.\n */\nexport interface PipelineOptions {\n readonly mediaType: 'video' | 'audio';\n readonly config: PlaybackConfig;\n readonly clock: ClockSource;\n readonly sync: SyncController;\n readonly onCommand: (cmd: DecoderCommand) => void;\n readonly onEvent: (evt: PlaybackEvent) => void;\n /** Optional recovery controller for escalation decisions. */\n readonly recovery?: RecoveryController;\n /** Video-only mode: set video reference from first CaptureTimestamp. */\n readonly videoOnly?: boolean;\n /** Live stream: enables bounded release + backlog shedding. Default: true. */\n readonly isLive?: boolean;\n}\n\n/**\n * Playback pipeline orchestrator.\n *\n * @see draft-ietf-moq-loc-01 §4.2\n */\nexport class PlaybackPipeline {\n private readonly mediaType: 'video' | 'audio';\n private readonly clock: ClockSource;\n private readonly sync: SyncController;\n private readonly onCommand: (cmd: DecoderCommand) => void;\n private readonly onEvent: (evt: PlaybackEvent) => void;\n private readonly recovery: RecoveryController | undefined;\n\n private readonly buffer: JitterBuffer;\n private readonly gapDetector: GapDetector;\n private readonly decoderState: DecoderStateMachine;\n\n /** Headers stored alongside objects, keyed by \"groupId:objectId\". */\n private readonly headerMap = new Map<string, LocHeaders>();\n\n /** Group reference counts in the jitter buffer. */\n private readonly bufferedGroupCounts = new Map<bigint, number>();\n\n /** Next expected objectId within the current group for contiguity check. */\n private nextExpectedObjectId = 0n;\n\n /** Highest group from which at least one object was consumed. -1n = nothing consumed yet. */\n private lastConsumedGroupId = -1n;\n\n /** Groups for which END_OF_GROUP has been observed. */\n private readonly endedGroups = new Set<bigint>();\n\n /** Timestamp when we first started waiting for active group completion. */\n private activeGroupWaitStartUs: number | null = null;\n\n /**\n * Minimum group ID accepted by pushObject().\n * Set after SKIP_FORWARD to reject stale objects from pre-skip groups\n * that arrive late due to async QUIC stream delivery.\n */\n private minAcceptGroupId = -1n;\n\n /** Last group for which keyframe_waiting was emitted (dedup). */\n private lastKeyframeWaitingGroupId = -1n;\n\n /**\n * When true, the first tick syncs to whatever group is available\n * without gap detection — avoids cascading recovery on initial\n * live join or after pause→resume on a live stream.\n *\n * Starts `true` so the first arriving group is accepted as the\n * starting point (joining mid-broadcast is not a gap).\n */\n private resetPending = true;\n\n /** True once track_ended has been emitted — prevents re-emission on every tick. */\n private _trackEnded = false;\n\n /** Last emitted playback rate — dedup to avoid spamming commands. */\n private _lastEmittedRate = 1.0;\n\n /**\n * Throttle flag — set by decoder feedback when queue depth is high.\n * While true, tick() evaluates gaps but does NOT drain the buffer.\n * Objects still enter the jitter buffer (needed for gap detection).\n */\n private _throttled = false;\n\n /** Adaptive jitter tolerance controller (null if disabled). */\n private readonly adaptiveTolerance: AdaptiveToleranceController | null;\n\n /** Max objects to release per tick (video only). 0 = unlimited. */\n private readonly maxReleasePerTick: number;\n /** Max groups before shedding old ones. 0 = unlimited. */\n private readonly maxBacklogGroups: number;\n\n /** Video codec string from catalog (e.g., 'avc1.42c01f'). Used for keyframe validation. */\n private _videoCodec: string | undefined;\n private _videoOnly: boolean;\n\n constructor(opts: PipelineOptions) {\n this.mediaType = opts.mediaType;\n this.clock = opts.clock;\n this.sync = opts.sync;\n this.onCommand = opts.onCommand;\n this.onEvent = opts.onEvent;\n this.recovery = opts.recovery;\n this._videoOnly = opts.videoOnly ?? false;\n\n // Bounded release: live video defaults to 5 objects/tick + 3 max\n // backlog groups. VOD/non-live and audio are unlimited — shedding\n // old groups is wrong when you need the full timeline.\n const liveVideo = opts.mediaType === 'video' && (opts.isLive ?? true);\n this.maxReleasePerTick = liveVideo\n ? (opts.config.maxReleasePerTick ?? 5)\n : 0;\n this.maxBacklogGroups = liveVideo\n ? (opts.config.maxBacklogGroups ?? 3)\n : 0;\n\n this.buffer = new JitterBuffer(opts.config.maxBufferDepth);\n this.gapDetector = new GapDetector({\n gapTimeoutUs: opts.config.gapTimeoutUs,\n clock: opts.clock,\n });\n this.decoderState = new DecoderStateMachine(opts.mediaType);\n\n // Adaptive tolerance: auto-calibrate gap timeout and drift threshold\n this.adaptiveTolerance = opts.config.adaptiveTolerance\n ? new AdaptiveToleranceController(DEFAULT_TOLERANCE_CONFIG)\n : null;\n\n // Startup buffer not used — video join re-anchor handles startup timing.\n }\n\n /**\n * Set the video codec string for keyframe payload validation.\n *\n * @param codec Codec string from catalog (e.g., 'avc1.42c01f', 'hvc1', 'av01.0.08M.10')\n * @see draft-ietf-moq-loc-01 §4.2 (decode order)\n */\n setCodec(codec: string): void {\n this._videoCodec = codec;\n }\n\n /**\n * Current effective gap timeout in microseconds.\n * When adaptive tolerance is enabled, this is auto-calibrated from jitter.\n * Otherwise, returns the fixed configured value.\n */\n get effectiveGapTimeoutUs(): number {\n if (this.adaptiveTolerance) {\n return this.adaptiveTolerance.effectiveGapTimeoutMs * 1000;\n }\n return this.gapDetector.gapTimeoutUs;\n }\n\n /** Last consumed group ID — for track switch subscription filters. */\n get currentGroupId(): bigint {\n return this.lastConsumedGroupId;\n }\n\n /** Number of distinct groups currently in the jitter buffer. */\n get bufferedGroupCount(): number {\n return this.bufferedGroupCounts.size;\n }\n\n /** Whether draining is throttled due to decoder backpressure. */\n get throttled(): boolean {\n return this._throttled;\n }\n\n /**\n * Handle feedback from browser adapters (decoder queue, errors, drift).\n *\n * @see draft-ietf-moq-loc-01 §2.3.1.1 (drift detection)\n */\n handleFeedback(fb: DecoderFeedback): void {\n switch (fb.type) {\n case 'queue_pressure':\n this._throttled = fb.depth >= fb.maxRecommended;\n break;\n\n case 'decode_error':\n // Clear throttle — the decoder has likely been recreated with an\n // empty queue. Without this, the pipeline deadlocks: throttled →\n // no decode commands → no decoder output → no queue_pressure_low\n // feedback → throttled forever.\n this._throttled = false;\n\n // Reset decoder FSM — the browser adapter has recreated the\n // WebCodecs decoder with an empty queue. Video must wait for\n // the next keyframe; audio resumes immediately (all key).\n // Without this, the FSM stays in DECODING and feeds delta\n // frames to a fresh decoder with no keyframe context → silent\n // failures → permanent stall.\n {\n const decision = this.decoderState.notifyGap();\n this.emitFsmDecision(decision);\n }\n\n if (this.recovery) {\n const action = this.recovery.evaluate({ type: 'decode_error', message: fb.message });\n this.onEvent({ type: 'recovery', action });\n }\n break;\n\n case 'frame_rendered':\n // PRESENTATION SCHEDULE drift: actual minus the exact time this\n // frame was queued to present. Both are on the SAME local clock,\n // so the old PTS-era guards no longer apply and are deliberately\n // gone: a capture timestamp is not required, an active video-join\n // offset is already represented in the schedule, and there is no\n // cross-epoch case to hide.\n //\n // Without a schedule the whole measurement/event transaction is\n // suppressed — returning BEFORE reading drift, so a stale value\n // from an earlier frame cannot be re-emitted as a new event.\n if (fb.scheduledRenderUs !== undefined) {\n this.sync.reportPresentationTiming(fb.scheduledRenderUs, fb.actualRenderUs);\n const drift = this.sync.currentDriftUs;\n if (this.sync.presentationDriftExceeded) {\n this.onEvent({ type: 'sync_drift', driftUs: drift });\n }\n }\n break;\n\n case 'flush_complete':\n // No-op — included for forward compatibility (quality switch gating).\n break;\n }\n }\n\n /**\n * Provide codec configuration (from catalog or initial headers).\n *\n * This is also triggered automatically when a LOC object carries the\n * matching config property: Video Config for video, Audio Config for\n * audio.\n *\n * @param config Codec-specific configuration bytes\n */\n configure(config: Uint8Array): void {\n const decision = this.decoderState.configure(config);\n if (decision.action === 'configure') {\n this.onCommand({\n type: 'configure',\n mediaType: this.mediaType,\n config,\n });\n }\n }\n\n /**\n * Push an object into the pipeline.\n *\n * Data objects are buffered. Gap objects notify the gap detector.\n *\n * @param obj The MoqtObject (data or gap)\n * @param headers Parsed LocHeaders (only meaningful for data objects)\n */\n pushObject(obj: MoqtObject, headers?: LocHeaders): void {\n if (obj.kind === 'gap') {\n // Gap signals — notify gap detector\n if (obj.status === ObjectStatus.END_OF_GROUP) {\n this.gapDetector.observeEndOfGroup(obj.groupId);\n this.endedGroups.add(obj.groupId as bigint);\n } else if (obj.status === ObjectStatus.END_OF_TRACK) {\n this.gapDetector.observeEndOfTrack();\n }\n return;\n }\n\n // Reject stale objects: both from before skip/reset targets AND\n // from groups already consumed during normal forward progress.\n // Late-arriving old groups from QUIC reordering can poison decode\n // order and cause blockiness.\n if (obj.groupId < this.minAcceptGroupId) return;\n if (this.lastConsumedGroupId >= 0n && obj.groupId < this.lastConsumedGroupId) return;\n // Reject already-consumed objects from the current group.\n if (obj.groupId === this.lastConsumedGroupId && obj.objectId < this.nextExpectedObjectId) return;\n\n // Dedup: reject if this (groupId, objectId) is already buffered.\n const key = `${obj.groupId}:${obj.objectId}`;\n if (this.headerMap.has(key)) return;\n\n // Data object — try to buffer it\n if (this.buffer.insert(obj)) {\n this.headerMap.set(key, headers ?? {});\n const count = this.bufferedGroupCounts.get(obj.groupId) ?? 0;\n this.bufferedGroupCounts.set(obj.groupId, count + 1);\n } else if (obj.publisherPriority !== undefined) {\n // Buffer full — try priority-aware eviction (§7)\n if (!this.tryEvictAndInsert(obj, headers) && this.recovery) {\n this.onEvent({\n type: 'recovery',\n action: this.recovery.evaluate({ type: 'buffer_overflow' }),\n });\n }\n } else if (this.recovery) {\n // No priority info, can't evict — notify recovery\n this.onEvent({\n type: 'recovery',\n action: this.recovery.evaluate({ type: 'buffer_overflow' }),\n });\n }\n\n // Interarrival jitter uses transit differences, so the timestamp epoch\n // cancels out. Media time is suitable here, but not for latency/catch-up.\n if (this.adaptiveTolerance && headers?.captureTimestamp !== undefined) {\n const nowMs = this.clock.now() / 1000; // µs → ms\n const captureUs = Number(headers.captureTimestamp);\n this.adaptiveTolerance.onFrameArrived(nowMs, captureUs, nowMs);\n }\n\n // Observe group for gap timeout tracking\n this.gapDetector.observeGroup(obj.groupId);\n }\n\n /**\n * Reset pipeline state for pause→resume or seek.\n *\n * Clears the jitter buffer, gap detector, and decoder FSM so the first\n * arriving group after resume is accepted cleanly without cascading\n * gap-recovery escalation from the stale pre-pause position.\n *\n * Emits a decoder 'reset' command so the WebCodecs decoder clears its\n * queue and waits for the next keyframe (video) or resumes immediately\n * (audio — Opus/AAC-LC frames are independently decodable).\n *\n * @param targetGroupId When seeking, set this to the target group ID.\n * Objects from groups before this ID are rejected — prevents stale\n * in-flight objects from corrupting the post-seek decoder state.\n * @see draft-ietf-moq-transport-16 §9.11.1 — \"it might still receive\n * Objects outside the new range if the publisher sent them before the\n * update was processed.\"\n */\n /**\n * Reset for a track switch — clears jitter buffer (old-track objects\n * use the same groupIds as new-track per altGroup alignment), resets\n * gap detector and group tracking. Does NOT reset decoder state or\n * emit flush/reset commands — already-decoded frames in the renderer\n * queue play out naturally for seamless visual transition.\n *\n * @param firstGroupId The first group ID from the new track — sets\n * lastConsumedGroupId to firstGroupId-1 so the gap detector doesn't\n * see a gap and trigger skip_forward (which would reset sync).\n */\n resetForTrackSwitch(firstGroupId: bigint): void {\n // Clear the jitter buffer — old-track encoded objects must not\n // survive into the new decoder configuration. Without this,\n // stale P-frames from the old track drain under the new codec\n // and produce decode errors.\n this.buffer.clear();\n this.headerMap.clear();\n this.bufferedGroupCounts.clear();\n this.lastConsumedGroupId = firstGroupId - 1n;\n this.endedGroups.add(this.lastConsumedGroupId);\n this.nextExpectedObjectId = 0n;\n this.minAcceptGroupId = firstGroupId;\n this.lastKeyframeWaitingGroupId = -1n;\n this._throttled = false;\n this.activeGroupWaitStartUs = null;\n this.gapDetector.reset();\n // Force decoder back to NEEDS_KEYFRAME so partial-group deltas\n // from a mid-stream subscription are skipped, not decoded.\n const decision = this.decoderState.notifyGap();\n this.emitFsmDecision(decision);\n }\n\n reset(targetGroupId?: bigint): void {\n this.buffer.clear();\n this.headerMap.clear();\n this.bufferedGroupCounts.clear();\n this.lastConsumedGroupId = -1n;\n this.nextExpectedObjectId = 0n;\n this.minAcceptGroupId = targetGroupId ?? -1n;\n this.lastKeyframeWaitingGroupId = -1n;\n this.resetPending = true;\n this._throttled = false;\n this._trackEnded = false;\n this.endedGroups.clear();\n this.activeGroupWaitStartUs = null;\n this.gapDetector.reset();\n this.adaptiveTolerance?.reset();\n const decision = this.decoderState.notifyGap();\n this.emitFsmDecision(decision);\n }\n\n /**\n * Process buffered objects: evaluate gaps, feed decoder FSM, emit commands.\n *\n * Call this on a regular interval (e.g., requestAnimationFrame or timer).\n */\n tick(): void {\n // Update adaptive tolerance → push auto-calibrated thresholds to gap detector and sync\n if (this.adaptiveTolerance) {\n const nowMs = this.clock.now() / 1000;\n this.adaptiveTolerance.tick(nowMs);\n this.gapDetector.gapTimeoutUs = this.adaptiveTolerance.effectiveGapTimeoutMs * 1000;\n this.sync.driftThresholdUs = this.adaptiveTolerance.effectiveDriftThresholdMs * 1000;\n }\n\n // After reset (pause→resume, jump-to-live), sync up to the first\n // group that starts with a keyframe (objectId 0). Stale P-frames\n // from the old subscription that arrive after the reset must not\n // become the new starting point — they lack keyframe context and\n // would produce artifacts.\n if (this.resetPending && this.buffer.size > 0) {\n const top = this.buffer.peek()!;\n // Only accept a group that starts with objectId 0 (keyframe).\n // Stale mid-group objects from the old subscription are\n // discarded by extracting + dropping them.\n if (top.objectId !== 0n) {\n this.buffer.extract();\n this.decrementGroupCount(top.groupId);\n return; // wait for a keyframe-starting group\n }\n this.lastConsumedGroupId = top.groupId - 1n;\n this.endedGroups.add(this.lastConsumedGroupId);\n this.resetPending = false;\n }\n\n // 1. Evaluate gap state\n const availableGroupIds = [...this.bufferedGroupCounts.keys()];\n const gapDecision = this.gapDetector.evaluate(this.lastConsumedGroupId, availableGroupIds);\n\n // 2. Handle gap decision\n if (gapDecision.action === GapAction.TRACK_ENDED) {\n if (!this._trackEnded) {\n this._trackEnded = true;\n const decision = this.decoderState.notifyTrackEnded();\n this.emitFsmDecision(decision);\n this.onEvent({ type: 'track_ended' });\n }\n return;\n }\n\n if (gapDecision.action === GapAction.SKIP_FORWARD && gapDecision.targetGroupId !== undefined) {\n const target = gapDecision.targetGroupId;\n\n // Emit partial_group_abandoned if the current video group wasn't\n // ended normally. This makes the transition observable for diagnostics.\n if (this.mediaType === 'video'\n && this.lastConsumedGroupId >= 0n\n && !this.endedGroups.has(this.lastConsumedGroupId)) {\n this.onEvent({\n type: 'partial_group_abandoned',\n fromGroupId: this.lastConsumedGroupId,\n toGroupId: target,\n reason: 'gap timeout',\n } as PlaybackEvent);\n }\n // Clean up ended-group tracking for skipped groups\n for (const gid of this.endedGroups) {\n if (gid < target) this.endedGroups.delete(gid);\n }\n\n this.onEvent({\n type: 'skip_forward',\n fromGroupId: this.lastConsumedGroupId,\n toGroupId: target,\n });\n\n // Notify recovery controller for potential escalation\n if (this.recovery) {\n const action = this.recovery.evaluate({ type: 'gap', groupId: target });\n this.onEvent({ type: 'recovery', action });\n }\n\n // Reject future pushObject() calls for groups before the target\n this.minAcceptGroupId = target;\n\n // Discard old groups from buffer\n this.discardBefore(target);\n\n // NOTE: Sync reset is NOT done here. When two pipelines (audio + video)\n // share a SyncController, both would call reset() during skip_forward,\n // causing a double-reset: audio resets → re-establishes reference →\n // video resets → wipes reference. The caller (MoqtPlayer) coordinates\n // sync reset via handlePipelineEvent, resetting once per tick cycle.\n\n // Notify decoder of gap\n const decision = this.decoderState.notifyGap();\n this.emitFsmDecision(decision);\n\n // Advance consumed position so we start from the target group\n this.lastConsumedGroupId = target - 1n;\n this.endedGroups.add(this.lastConsumedGroupId);\n this.nextExpectedObjectId = 0n;\n this.activeGroupWaitStartUs = null;\n }\n\n // 3. Backlog shedding: runs BEFORE throttle check so backlog is\n // controlled even when decoder pressure is high. Without this,\n // objects keep entering via pushObject() while throttled, and the\n // shedding path never executes — defeating \"move the control\n // point earlier.\"\n if (this.maxBacklogGroups > 0 && this.bufferedGroupCounts.size > this.maxBacklogGroups) {\n this.shedBacklog();\n }\n\n // 4. Throttle: if decoder queue is deep, skip draining.\n // Objects still enter via pushObject() (needed for gap detection).\n // Un-throttle signals come from CommandDispatcher on frame output —\n // as the decoder drains its queue, checkQueuePressure() fires on each\n // decoded frame, crossing the low threshold to clear the throttle.\n // @see draft-ietf-moq-transport-16 §7\n if (this._throttled) return;\n\n // 4b. Intra-group timeout: abandon a partially-consumed video group\n // when its suffix never arrives. The gap detector handles inter-group\n // gaps (N+2 present, N+1 missing). This handles the case where N+1\n // is present but N is incomplete — the gap detector won't fire because\n // the next group IS available.\n if (this.mediaType === 'video' && this.activeGroupWaitStartUs !== null) {\n const elapsed = this.clock.now() - this.activeGroupWaitStartUs;\n if (elapsed > this.gapDetector.gapTimeoutUs) {\n const nextGroupId = this.lastConsumedGroupId + 1n;\n this.onEvent({\n type: 'partial_group_abandoned',\n fromGroupId: this.lastConsumedGroupId,\n toGroupId: nextGroupId,\n reason: 'intra-group timeout',\n } as PlaybackEvent);\n this.endedGroups.add(this.lastConsumedGroupId);\n const decision = this.decoderState.notifyGap();\n this.emitFsmDecision(decision);\n this.activeGroupWaitStartUs = null;\n }\n }\n\n // 5. Release contiguous objects from the buffer (bounded).\n // Within a group, objects MUST be contiguous (0, 1, 2, ...) before\n // decoding. A missing objectId means a frame the decoder needs for\n // reference is absent — decoding later objects produces artifacts.\n // Wait for the missing object (QUIC delivers in-order per stream)\n // or let the gap detector timeout and skip the group.\n let released = 0;\n const budget = this.maxReleasePerTick;\n\n while (this.buffer.size > 0) {\n // Bounded release: stop after budget objects (0 = unlimited).\n if (budget > 0 && released >= budget) break;\n // Mid-tick throttle: decoder feedback during processDataObject\n // may set _throttled via handleFeedback(queue_pressure).\n if (this._throttled) break;\n\n const top = this.buffer.peek();\n if (!top) break;\n\n const nextExpectedGroup = this.lastConsumedGroupId + 1n;\n\n // Don't drain past the next expected group — there might be a gap\n if (top.groupId > nextExpectedGroup) {\n break;\n }\n\n // New group boundary: for video, don't silently transition from\n // a partial group. If the current group wasn't ended (END_OF_GROUP),\n // wait for the intra-group timeout to explicitly abandon it.\n // Audio is independently decodable — transitions are always safe.\n if (top.groupId > this.lastConsumedGroupId && this.lastConsumedGroupId >= 0n) {\n if (this.mediaType === 'video' && !this.endedGroups.has(this.lastConsumedGroupId)) {\n if (this.activeGroupWaitStartUs === null) {\n this.activeGroupWaitStartUs = this.clock.now();\n }\n break;\n }\n this.activeGroupWaitStartUs = null;\n this.endedGroups.delete(this.lastConsumedGroupId);\n }\n\n // New group: reset objectId contiguity tracker\n if (top.groupId > this.lastConsumedGroupId) {\n this.nextExpectedObjectId = 0n;\n }\n\n // Intra-group contiguity (video only): if object N is missing\n // but N+1 is available, skip the rest of this group — decoding\n // N+1 without N's reference produces artifacts. Audio chunks\n // are independently decodable (LOC §4.1) so gaps are harmless.\n if (this.mediaType === 'video' && top.objectId > this.nextExpectedObjectId) {\n // Discard remaining objects in this group — they can't\n // decode correctly without the missing reference.\n const skipGroupId = top.groupId;\n while (this.buffer.size > 0) {\n const peek = this.buffer.peek();\n if (!peek || peek.groupId !== skipGroupId) break;\n this.buffer.extract();\n this.decrementGroupCount(skipGroupId);\n }\n this.lastConsumedGroupId = skipGroupId;\n this.nextExpectedObjectId = 0n;\n // Let decoder know it needs a fresh keyframe\n const decision = this.decoderState.notifyGap();\n this.emitFsmDecision(decision);\n break;\n }\n\n const obj = this.buffer.extract()!;\n this.decrementGroupCount(obj.groupId);\n\n if (obj.kind === 'data') {\n this.processDataObject(obj);\n }\n\n this.nextExpectedObjectId = obj.objectId + 1n;\n released++;\n\n // Track last consumed group\n if (obj.groupId > this.lastConsumedGroupId) {\n this.lastConsumedGroupId = obj.groupId;\n }\n }\n }\n\n // ─── Internal ───────────────────────────────────────────────────\n\n /**\n * Drop oldest whole groups when backlog exceeds maxBacklogGroups.\n * Keeps the newest maxBacklogGroups groups, discards the rest.\n * After shedding, notifies decoder that a gap occurred (needs keyframe).\n */\n private shedBacklog(): void {\n const groupIds = [...this.bufferedGroupCounts.keys()].sort((a, b) =>\n a < b ? -1 : a > b ? 1 : 0,\n );\n const excess = groupIds.length - this.maxBacklogGroups;\n if (excess <= 0) return;\n\n const groupsToShed = groupIds.slice(0, excess);\n const cutoff = groupsToShed[groupsToShed.length - 1]! + 1n;\n this.buffer.discardBefore(cutoff);\n for (const gid of groupsToShed) {\n this.bufferedGroupCounts.delete(gid);\n }\n // Clean up leaked header entries for shed groups\n for (const key of this.headerMap.keys()) {\n const groupId = BigInt(key.split(':')[0]!);\n if (groupId < cutoff) this.headerMap.delete(key);\n }\n\n // Advance consumed position past shed groups\n const lastShed = groupsToShed[groupsToShed.length - 1]!;\n if (lastShed >= this.lastConsumedGroupId) {\n this.lastConsumedGroupId = lastShed;\n }\n this.minAcceptGroupId = lastShed + 1n;\n\n // Decoder needs a fresh keyframe after shedding\n const decision = this.decoderState.notifyGap();\n this.emitFsmDecision(decision);\n\n this.onEvent({\n type: 'backlog_shed',\n droppedGroups: excess,\n remainingGroups: this.bufferedGroupCounts.size,\n reason: `backlog exceeded ${this.maxBacklogGroups} groups`,\n });\n }\n\n /**\n * Whether late encoded VIDEO may be discarded before it reaches the decoder.\n *\n * Only explicitly discardable, non-independent frames may be. Everything\n * else is NOT PROVEN SAFE TO DISCARD and is therefore preserved\n * conservatively: RFC 9626 §3.1 defines D=1 as the sender asserting the\n * frame can be removed while the stream stays decodable, so D=0 does not\n * prove the frame is a reference — it means the receiver has no assurance\n * that discarding it is safe. Absent marking is treated the same way, and\n * an independent frame is preserved even if it also claims to be\n * discardable.\n *\n * Lateness of decoded OUTPUT remains the output stage's decision.\n *\n * @see draft-ietf-moq-loc-01 §2.3.2.2 (Video Frame Marking)\n */\n private isPreDecodeDroppable(marking: VideoFrameMarking | undefined): boolean {\n if (marking === undefined) return false; // unknown dependencies — keep\n if (marking.independent) return false; // reference anchor — keep\n return marking.discardable === true; // sender said it is safe\n }\n\n /**\n * Attempt to evict a lower-importance object to make room.\n * @returns true if the incoming object was inserted.\n * @see draft-ietf-moq-transport-16 §7 (Priority-based dropping)\n */\n private tryEvictAndInsert(obj: MoqtObjectData, headers?: LocHeaders): boolean {\n const evicted = this.buffer.evictLowestImportance();\n if (!evicted) return false; // No evictable candidate\n\n if (evicted.publisherPriority! > obj.publisherPriority!) {\n // Evicted is less important — insert incoming\n this.decrementGroupCount(evicted.groupId);\n const evictedKey = `${evicted.groupId}:${evicted.objectId}`;\n this.headerMap.delete(evictedKey);\n\n this.buffer.insert(obj);\n const key = `${obj.groupId}:${obj.objectId}`;\n this.headerMap.set(key, headers ?? {});\n const count = this.bufferedGroupCounts.get(obj.groupId) ?? 0;\n this.bufferedGroupCounts.set(obj.groupId, count + 1);\n return true;\n } else {\n // Evicted is equally or more important — put it back, drop incoming\n this.buffer.insert(evicted);\n return false;\n }\n }\n\n private processDataObject(obj: MoqtObjectData): void {\n const key = `${obj.groupId}:${obj.objectId}`;\n const headers = this.headerMap.get(key) ?? {};\n this.headerMap.delete(key);\n\n // 1. Configure decoder if the object carries codec config for this\n // media type. DecoderStateMachine.configure dedupes equal bytes.\n // @see draft-ietf-moq-loc-04 §2.3.2.1 (Video Config), §2.3.3.1 (Audio Config)\n const config = this.mediaType === 'video' ? headers.videoConfig : headers.audioConfig;\n if (config) {\n const configDecision = this.decoderState.configure(config);\n if (configDecision.action === 'configure') {\n this.onCommand({\n type: 'configure',\n mediaType: this.mediaType,\n config,\n });\n }\n }\n\n // 2. Establish sync reference.\n // Audio-master: first audio sample sets the reference (ONCE).\n // Video-only: first video CaptureTimestamp sets the reference.\n if (headers.captureTimestamp !== undefined && !this.sync.hasReference) {\n if (this.mediaType === 'audio') {\n this.sync.setAudioReference(headers.captureTimestamp);\n } else if (this._videoOnly) {\n this.sync.setVideoReference(headers.captureTimestamp);\n }\n }\n\n // 3. Compute render time.\n // Returns null when no reference — CommandDispatcher holds the frame.\n // Video dependency marking, resolved BEFORE the lateness decision so the\n // drop policy and DecoderStateMachine see the same marking.\n const videoMarking = this.mediaType === 'video'\n ? (headers.videoFrameMarking\n ?? (obj.objectId === 0n\n ? { independent: true, startOfFrame: true, endOfFrame: true,\n discardable: false, baseLayerSync: false, temporalId: 0 }\n : undefined))\n : undefined;\n\n let renderTimeUs: number;\n if (headers.captureTimestamp !== undefined) {\n const timing = this.mediaType === 'video'\n ? this.sync.computeVideoRenderTime(headers.captureTimestamp)\n : this.sync.computeAudioRenderTime(headers.captureTimestamp);\n\n if (timing === null) {\n // No reference yet — use placeholder. CommandDispatcher will\n // hold this frame and recompute when the reference is ready.\n renderTimeUs = 0;\n } else if (timing.shouldDrop) {\n if (timing.offsetUs < -5_000_000) {\n renderTimeUs = this.clock.now();\n } else if (this.mediaType === 'video' && !this.isPreDecodeDroppable(videoMarking)) {\n // Late, but the sender has not asserted this frame can be\n // removed with the stream still decodable, so discarding it\n // here risks every frame that may depend on it — a far\n // larger cost than one late picture. Decode it and let the\n // OUTPUT stage decide what to present: the dispatcher\n // recomputes timing at decoder output and CanvasRenderer\n // drops sufficiently late frames while keeping the newest.\n renderTimeUs = this.clock.now();\n } else {\n return; // Genuinely late and safe to discard — skip\n }\n } else if (timing.offsetUs > 5_000_000) {\n renderTimeUs = this.clock.now();\n } else {\n renderTimeUs = timing.renderTimeUs;\n }\n } else {\n renderTimeUs = this.clock.now();\n }\n\n // 3b. Evaluate live catch-up from CaptureTimestamp\n // @see draft-ietf-moq-loc-01 §2.3.1.1 (latency measurement)\n // @see draft-ietf-moq-msf-00 §5.1.16 (targetLatency)\n if (headers.captureTimestamp !== undefined) {\n const catchUp = this.sync.evaluateCatchUp(headers.captureTimestamp, headers.timestampIsWallClock);\n if (catchUp !== null && catchUp.currentRate !== this._lastEmittedRate) {\n this._lastEmittedRate = catchUp.currentRate;\n this.onEvent({ type: 'catch_up_changed', state: catchUp });\n this.onCommand({ type: 'set_playback_rate', rate: catchUp.currentRate });\n }\n }\n\n // 4. Create chunk init and feed through decoder FSM\n if (this.mediaType === 'video') {\n // Resolved above, before the lateness decision, so the drop policy\n // and the decoder FSM cannot disagree about the same object.\n const marking = videoMarking;\n // Keyframe payload validation: verify bitstream actually starts with a keyframe\n // when the LOC header claims independent. Catches broken publishers.\n // Only check payloads > 5 bytes (too short = can't determine).\n // @see draft-ietf-moq-loc-01 §4.2\n if (marking?.independent && this._videoCodec && obj.payload.length > 5) {\n const isKeyframe = isKeyframePayload(this._videoCodec, obj.payload);\n if (isKeyframe === false) {\n this.onEvent({\n type: 'keyframe_validation_failed',\n groupId: obj.groupId,\n objectId: obj.objectId,\n codec: this._videoCodec,\n });\n }\n }\n\n const headersWithMarking = marking !== undefined\n ? { ...headers, videoFrameMarking: marking }\n : headers;\n const chunk = toVideoChunkInit(obj.payload, headersWithMarking);\n const prevState = this.decoderState.state;\n const decision = this.decoderState.processVideoChunk(chunk, marking);\n\n // Detect live join: NEEDS_KEYFRAME → DECODING (first keyframe decoded)\n // Apply video join re-anchor to prevent startup stutter.\n if (prevState === 'needs_keyframe' && this.decoderState.state === 'decoding'\n && headers.captureTimestamp !== undefined) {\n this.sync.onVideoJoin(headers.captureTimestamp);\n // Recompute render time with the offset applied\n const recomputed = this.sync.computeVideoRenderTime(headers.captureTimestamp);\n if (recomputed) {\n renderTimeUs = recomputed.renderTimeUs;\n }\n }\n\n if (decision.action === 'decode') {\n this.onCommand({\n type: 'decode_video',\n chunk: decision.chunk as typeof chunk,\n renderTimeUs,\n ...(headers.captureTimestamp !== undefined\n ? { captureTimestampUs: headers.captureTimestamp }\n : {}),\n });\n } else if (decision.action === 'skip') {\n // Emit keyframe_waiting once per group (avoid per-object spam)\n if (decision.reason === 'waiting for keyframe' && obj.groupId !== this.lastKeyframeWaitingGroupId) {\n this.lastKeyframeWaitingGroupId = obj.groupId;\n this.onEvent({ type: 'keyframe_waiting', groupId: obj.groupId });\n }\n }\n } else {\n const chunk = toAudioChunkInit(obj.payload, headers);\n const decision = this.decoderState.processAudioChunk(chunk);\n\n if (decision.action === 'decode') {\n this.onCommand({\n type: 'decode_audio',\n chunk: decision.chunk as typeof chunk,\n renderTimeUs,\n });\n }\n }\n }\n\n private emitFsmDecision(decision: { action: string; reason?: string }): void {\n if (decision.action === 'reset') {\n this.onCommand({\n type: 'reset',\n mediaType: this.mediaType,\n reason: (decision as any).reason ?? 'gap',\n });\n } else if (decision.action === 'flush') {\n this.onCommand({\n type: 'flush',\n mediaType: this.mediaType,\n });\n }\n }\n\n private discardBefore(groupId: bigint): void {\n // Remove from buffer\n this.buffer.discardBefore(groupId);\n\n // Clean up group counts and headers for removed groups\n for (const [gid] of this.bufferedGroupCounts) {\n if (gid < groupId) {\n this.bufferedGroupCounts.delete(gid);\n }\n }\n\n // Clean up headers for discarded objects\n for (const [key] of this.headerMap) {\n const colonIdx = key.indexOf(':');\n const gid = BigInt(key.slice(0, colonIdx));\n if (gid < groupId) {\n this.headerMap.delete(key);\n }\n }\n }\n\n private decrementGroupCount(groupId: bigint): void {\n const count = this.bufferedGroupCounts.get(groupId);\n if (count !== undefined) {\n if (count <= 1) {\n this.bufferedGroupCounts.delete(groupId);\n } else {\n this.bufferedGroupCounts.set(groupId, count - 1);\n }\n }\n }\n}\n","/**\n * BandwidthEstimator — EWMA-smoothed bandwidth estimation from\n * group arrival rate.\n *\n * MoQ/QUIC has no HTTP request/response cycle to time, so traditional\n * ABR segment-download bandwidth estimation doesn't apply. Instead we\n * measure the effective throughput from object arrival timing:\n *\n * instantaneous_bps = group_size_bytes * 8 / inter_arrival_seconds\n *\n * Smoothed with EWMA (Exponentially Weighted Moving Average) to filter\n * single-sample spikes and bursty delivery patterns (conference WiFi).\n *\n * The estimate drives proactive quality decisions — downshift BEFORE\n * stalls occur, based on arrival rate vs current track bitrate.\n *\n * @module\n */\n\nexport class BandwidthEstimator {\n private readonly alpha: number;\n private readonly minSamples: number;\n private estimateBps: number | null = null;\n private lastTimestampUs: number | null = null;\n private lastGroupBytes: number | null = null;\n private sampleCount = 0;\n\n /**\n * @param alpha EWMA smoothing factor (0-1). Higher = more responsive\n * to recent samples, lower = smoother. Default 0.3.\n * @param minSamples Minimum group arrivals before reporting a non-zero\n * estimate. Early samples include connection latency\n * and aren't representative. Default 5 (~10s at 2s GOPs).\n */\n constructor(alpha = 0.3, minSamples = 5) {\n this.alpha = alpha;\n this.minSamples = minSamples;\n }\n\n /**\n * Record a group arrival. Call once per complete group (GOP).\n *\n * @param bytes Total bytes in the group.\n * @param timestampUs Arrival timestamp in microseconds.\n */\n recordGroup(bytes: number, timestampUs: number): void {\n if (this.lastTimestampUs !== null && this.lastGroupBytes !== null) {\n const deltaUs = timestampUs - this.lastTimestampUs;\n if (deltaUs > 0) {\n this.sampleCount++;\n const instantBps = (bytes * 8) / (deltaUs / 1_000_000);\n if (this.estimateBps === null) {\n this.estimateBps = instantBps;\n } else {\n this.estimateBps = this.alpha * instantBps + (1 - this.alpha) * this.estimateBps;\n }\n }\n }\n this.lastTimestampUs = timestampUs;\n this.lastGroupBytes = bytes;\n }\n\n /**\n * Current smoothed bandwidth estimate in kbps.\n * Returns 0 if fewer than 2 groups have been recorded.\n */\n getEstimateKbps(): number {\n if (this.estimateBps === null || this.sampleCount < this.minSamples) return 0;\n return this.estimateBps / 1000;\n }\n\n /** Reset all state. */\n reset(): void {\n this.estimateBps = null;\n this.lastTimestampUs = null;\n this.lastGroupBytes = null;\n this.sampleCount = 0;\n }\n}\n","/**\n * BufferBasedController — hybrid ABR using buffer depth as the primary\n * signal and bandwidth estimation as a secondary corroboration signal.\n *\n * Buffer depth directly measures stall risk (the thing we care about).\n * Bandwidth estimation measures throughput (a proxy for stall risk that\n * can be noisy on bursty networks). Combining them:\n *\n * buffer < LOW → downshift immediately (emergency, bandwidth irrelevant)\n * buffer in [LOW, HIGH] → hold steady (buffer is fine, don't react to noise)\n * buffer > HIGH AND bandwidth > 2.25× next-higher → upshift (both agree)\n * buffer > HIGH AND bandwidth insufficient → hold (don't burn buffer)\n *\n * This avoids the EWMA over-reaction problem (bursty delivery tanks the\n * estimate but buffer is healthy) while still enabling proactive upshift\n * when conditions genuinely support it.\n *\n * @module\n */\n\nimport type { ClockSource } from './types.js';\n\n/** One entry in the ABR quality ladder. */\nexport interface AbrTrack {\n readonly name: string;\n readonly bitrateKbps: number;\n}\n\n/** Configuration for the buffer-based controller. */\nexport interface BufferBasedConfig {\n readonly clock: ClockSource;\n /** Quality ladder sorted highest-bitrate-first (index 0 = best). */\n readonly ladder: readonly AbrTrack[];\n /** Starting index into the ladder. */\n readonly initialIndex: number;\n /** Buffer depth below this → emergency downshift (µs). */\n readonly lowThresholdUs: number;\n /** Buffer depth above this → consider upshift (µs). */\n readonly highThresholdUs: number;\n /** Bandwidth must exceed this multiple of the target track's bitrate\n * for an upshift to be approved. Default 2.25. */\n readonly upshiftBandwidthMultiple?: number;\n}\n\n/** Input signals for an ABR evaluation. */\nexport interface AbrSignals {\n /** Current buffer depth in microseconds. */\n readonly bufferDepthUs: number;\n /** EWMA bandwidth estimate in kbps (0 = unknown). */\n readonly bandwidthEstimateKbps: number;\n}\n\n/** ABR decision. */\nexport interface AbrDecision {\n readonly action: 'downshift' | 'upshift' | 'hold';\n /** Target ladder index (only set for downshift/upshift). */\n readonly targetIndex?: number;\n}\n\nexport class BufferBasedController {\n private readonly ladder: readonly AbrTrack[];\n readonly lowThresholdUs: number;\n /** Kept for future temporal-buffer mode (playout buffer, not jitter buffer). */\n readonly highThresholdUs: number;\n private readonly upshiftMultiple: number;\n private currentIndex: number;\n\n constructor(config: BufferBasedConfig) {\n this.ladder = config.ladder;\n this.lowThresholdUs = config.lowThresholdUs;\n this.highThresholdUs = config.highThresholdUs;\n this.upshiftMultiple = config.upshiftBandwidthMultiple ?? 2.25;\n this.currentIndex = config.initialIndex;\n }\n\n /** Current ladder index. */\n get index(): number { return this.currentIndex; }\n\n /** Current track. */\n get currentTrack(): AbrTrack | undefined { return this.ladder[this.currentIndex]; }\n\n /**\n * Evaluate buffer depth + bandwidth and return an ABR decision.\n *\n * Called periodically (e.g., every tick or every group arrival).\n */\n /**\n * Evaluate buffer depth + bandwidth and return an ABR recommendation.\n *\n * Does NOT mutate internal state — the caller must call\n * {@link commitDownshift} or {@link commitUpshift} after a\n * confirmed switch. This prevents index drift when async switches\n * fail or are aborted.\n */\n evaluate(signals: AbrSignals): AbrDecision {\n // Emergency: buffer critically low → recommend downshift\n if (signals.bufferDepthUs < this.lowThresholdUs) {\n if (this.currentIndex < this.ladder.length - 1) {\n return { action: 'downshift', targetIndex: this.currentIndex + 1 };\n }\n return { action: 'hold' }; // already at lowest\n }\n\n // Upshift: buffer must be above the high threshold — asymmetric\n // hysteresis so upshift is much harder to trigger than downshift.\n // AND bandwidth must corroborate.\n if (this.currentIndex > 0 && signals.bufferDepthUs >= this.highThresholdUs) {\n const higherTrack = this.ladder[this.currentIndex - 1]!;\n if (signals.bandwidthEstimateKbps > higherTrack.bitrateKbps * this.upshiftMultiple) {\n return { action: 'upshift', targetIndex: this.currentIndex - 1 };\n }\n }\n\n return { action: 'hold' };\n }\n\n /** Confirm a downshift — advances index to the lower track. */\n commitDownshift(): void {\n if (this.currentIndex < this.ladder.length - 1) this.currentIndex++;\n }\n\n /** Confirm an upshift — moves index to the higher track. */\n commitUpshift(): void {\n if (this.currentIndex > 0) this.currentIndex--;\n }\n\n /** Reset to a specific ladder index (e.g., after manual selection). */\n setIndex(index: number): void {\n this.currentIndex = Math.max(0, Math.min(index, this.ladder.length - 1));\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACkBA,WAAS,QAAQ,GAAe,GAAuB;AACnD,QAAI,EAAE,UAAU,EAAE,QAAS,QAAO;AAClC,QAAI,EAAE,UAAU,EAAE,QAAS,QAAO;AAClC,QAAI,EAAE,WAAW,EAAE,SAAU,QAAO;AACpC,QAAI,EAAE,WAAW,EAAE,SAAU,QAAO;AACpC,WAAO;AAAA,EACX;AAOO,MAAM,eAAN,MAAmB;AAAA,IACL,OAAqB,CAAC;AAAA,IACtB;AAAA,IAEjB,YAAY,UAAkB;AAC1B,WAAK,WAAW;AAAA,IACpB;AAAA;AAAA,IAGA,IAAI,OAAe;AACf,aAAO,KAAK,KAAK;AAAA,IACrB;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,OAAO,KAA0B;AAC7B,UAAI,KAAK,KAAK,UAAU,KAAK,SAAU,QAAO;AAC9C,WAAK,KAAK,KAAK,GAAG;AAClB,WAAK,SAAS,KAAK,KAAK,SAAS,CAAC;AAClC,aAAO;AAAA,IACX;AAAA;AAAA;AAAA;AAAA,IAKA,OAA+B;AAC3B,aAAO,KAAK,KAAK,CAAC;AAAA,IACtB;AAAA;AAAA;AAAA;AAAA,IAKA,UAAkC;AAC9B,UAAI,KAAK,KAAK,WAAW,EAAG,QAAO;AACnC,YAAM,MAAM,KAAK,KAAK,CAAC;AACvB,YAAM,OAAO,KAAK,KAAK,IAAI;AAC3B,UAAI,KAAK,KAAK,SAAS,GAAG;AACtB,aAAK,KAAK,CAAC,IAAI;AACf,aAAK,SAAS,CAAC;AAAA,MACnB;AACA,aAAO;AAAA,IACX;AAAA;AAAA;AAAA;AAAA,IAKA,cAAc,SAAiB,UAA2B;AACtD,UAAI,KAAK,KAAK,WAAW,EAAG,QAAO;AACnC,YAAM,MAAM,KAAK,KAAK,CAAC;AACvB,aAAO,IAAI,YAAY,WAAW,IAAI,aAAa;AAAA,IACvD;AAAA;AAAA;AAAA;AAAA,IAKA,WAAW,SAA+B;AACtC,YAAM,SAAuB,CAAC;AAC9B,YAAM,YAA0B,CAAC;AAGjC,iBAAW,OAAO,KAAK,MAAM;AACzB,YAAI,IAAI,YAAY,SAAS;AACzB,iBAAO,KAAK,GAAG;AAAA,QACnB,OAAO;AACH,oBAAU,KAAK,GAAG;AAAA,QACtB;AAAA,MACJ;AAGA,aAAO,KAAK,OAAO;AAGnB,WAAK,KAAK,SAAS;AACnB,iBAAW,OAAO,WAAW;AACzB,aAAK,KAAK,KAAK,GAAG;AAAA,MACtB;AACA,WAAK,QAAQ;AAEb,aAAO;AAAA,IACX;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,cAAc,SAAyB;AACnC,YAAM,YAA0B,CAAC;AACjC,UAAI,UAAU;AAEd,iBAAW,OAAO,KAAK,MAAM;AACzB,YAAI,IAAI,UAAU,SAAS;AACvB;AAAA,QACJ,OAAO;AACH,oBAAU,KAAK,GAAG;AAAA,QACtB;AAAA,MACJ;AAEA,WAAK,KAAK,SAAS;AACnB,iBAAW,OAAO,WAAW;AACzB,aAAK,KAAK,KAAK,GAAG;AAAA,MACtB;AACA,WAAK,QAAQ;AAEb,aAAO;AAAA,IACX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAcA,wBAAoD;AAChD,UAAI,KAAK,KAAK,WAAW,EAAG,QAAO;AAEnC,UAAI,WAAW;AACf,UAAI,gBAAgB;AACpB,UAAI,eAAe,CAAC;AAEpB,eAAS,IAAI,GAAG,IAAI,KAAK,KAAK,QAAQ,KAAK;AACvC,cAAM,MAAM,KAAK,KAAK,CAAC;AACvB,YAAI,IAAI,SAAS,UAAU,IAAI,sBAAsB,OAAW;AAEhE,cAAM,IAAI,IAAI;AACd,YACI,IAAI,iBACH,MAAM,iBAAiB,IAAI,UAAU,cACxC;AACE,qBAAW;AACX,0BAAgB;AAChB,yBAAe,IAAI;AAAA,QACvB;AAAA,MACJ;AAEA,UAAI,aAAa,GAAI,QAAO;AAE5B,YAAM,UAAU,KAAK,KAAK,QAAQ;AAGlC,YAAM,OAAO,KAAK,KAAK,IAAI;AAC3B,UAAI,WAAW,KAAK,KAAK,QAAQ;AAC7B,aAAK,KAAK,QAAQ,IAAI;AACtB,aAAK,QAAQ;AAAA,MACjB;AAEA,aAAO;AAAA,IACX;AAAA;AAAA;AAAA;AAAA,IAKA,QAAc;AACV,WAAK,KAAK,SAAS;AAAA,IACvB;AAAA;AAAA,IAIQ,SAAS,OAAqB;AAClC,aAAO,QAAQ,GAAG;AACd,cAAM,YAAa,QAAQ,KAAM;AACjC,YAAI,QAAQ,KAAK,KAAK,KAAK,GAAI,KAAK,KAAK,SAAS,CAAE,KAAK,EAAG;AAC5D,aAAK,KAAK,OAAO,SAAS;AAC1B,gBAAQ;AAAA,MACZ;AAAA,IACJ;AAAA,IAEQ,SAAS,OAAqB;AAClC,YAAM,SAAS,KAAK,KAAK;AACzB,aAAO,MAAM;AACT,YAAI,WAAW;AACf,cAAM,OAAO,IAAI,QAAQ;AACzB,cAAM,QAAQ,IAAI,QAAQ;AAE1B,YAAI,OAAO,UAAU,QAAQ,KAAK,KAAK,IAAI,GAAI,KAAK,KAAK,QAAQ,CAAE,IAAI,GAAG;AACtE,qBAAW;AAAA,QACf;AACA,YAAI,QAAQ,UAAU,QAAQ,KAAK,KAAK,KAAK,GAAI,KAAK,KAAK,QAAQ,CAAE,IAAI,GAAG;AACxE,qBAAW;AAAA,QACf;AACA,YAAI,aAAa,MAAO;AACxB,aAAK,KAAK,OAAO,QAAQ;AACzB,gBAAQ;AAAA,MACZ;AAAA,IACJ;AAAA,IAEQ,KAAK,GAAW,GAAiB;AACrC,YAAM,MAAM,KAAK,KAAK,CAAC;AACvB,WAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC;AAC1B,WAAK,KAAK,CAAC,IAAI;AAAA,IACnB;AAAA;AAAA,IAGQ,UAAgB;AACpB,eAAS,KAAK,KAAK,KAAK,UAAU,KAAK,GAAG,KAAK,GAAG,KAAK;AACnD,aAAK,SAAS,CAAC;AAAA,MACnB;AAAA,IACJ;AAAA,EACJ;;;ACxNO,MAAM,YAAY;AAAA;AAAA,IAErB,MAAM;AAAA;AAAA,IAEN,cAAc;AAAA;AAAA,IAEd,aAAa;AAAA,EACjB;AAoBO,MAAM,cAAN,MAAkB;AAAA,IACb;AAAA,IACS;AAAA;AAAA,IAGA,cAAc,oBAAI,IAAY;AAAA;AAAA,IAGvC,aAAa;AAAA;AAAA,IAGJ,mBAAmB,oBAAI,IAAoB;AAAA,IAE5D,YAAY,QAAsD;AAC9D,WAAK,gBAAgB,OAAO;AAC5B,WAAK,QAAQ,OAAO;AAAA,IACxB;AAAA;AAAA,IAGA,IAAI,eAAuB;AAAE,aAAO,KAAK;AAAA,IAAe;AAAA,IACxD,IAAI,aAAa,OAAe;AAAE,WAAK,gBAAgB;AAAA,IAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAM9D,aAAa,SAAuB;AAChC,UAAI,CAAC,KAAK,iBAAiB,IAAI,OAAO,GAAG;AACrC,aAAK,iBAAiB,IAAI,SAAS,KAAK,MAAM,IAAI,CAAC;AAAA,MACvD;AAAA,IACJ;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,kBAAkB,SAAuB;AACrC,WAAK,YAAY,IAAI,OAAO;AAAA,IAChC;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,oBAA0B;AACtB,WAAK,aAAa;AAAA,IACtB;AAAA;AAAA,IAGA,IAAI,eAAwB;AACxB,aAAO,KAAK;AAAA,IAChB;AAAA;AAAA,IAGA,QAAc;AACV,WAAK,YAAY,MAAM;AACvB,WAAK,aAAa;AAClB,WAAK,iBAAiB,MAAM;AAAA,IAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,SAAS,qBAA6B,mBAA0C;AAE5E,UAAI,KAAK,YAAY;AACjB,eAAO,EAAE,QAAQ,UAAU,YAAY;AAAA,MAC3C;AAGA,UAAI,kBAAkB,WAAW,GAAG;AAChC,eAAO,EAAE,QAAQ,UAAU,KAAK;AAAA,MACpC;AAEA,YAAM,eAAe,sBAAsB;AAC3C,YAAM,SAAS,CAAC,GAAG,iBAAiB,EAAE,KAAK,CAAC,GAAG,MAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAE;AACjF,YAAM,iBAAiB,OAAO,CAAC;AAG/B,UAAI,kBAAkB,cAAc;AAChC,eAAO,EAAE,QAAQ,UAAU,KAAK;AAAA,MACpC;AAIA,UAAI,WAAW;AACf,eAAS,IAAI,cAAc,IAAI,gBAAgB,KAAK;AAChD,YAAI,CAAC,KAAK,YAAY,IAAI,CAAC,GAAG;AAC1B,qBAAW;AACX;AAAA,QACJ;AAAA,MACJ;AAEA,UAAI,UAAU;AAEV,eAAO,EAAE,QAAQ,UAAU,cAAc,eAAe,eAAe;AAAA,MAC3E;AAIA,YAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,YAAM,cAAc,KAAK,iBAAiB,IAAI,cAAc;AAE5D,UAAI,gBAAgB,QAAW;AAC3B,cAAM,UAAU,MAAM;AACtB,YAAI,WAAW,KAAK,cAAc;AAC9B,iBAAO,EAAE,QAAQ,UAAU,cAAc,eAAe,eAAe;AAAA,QAC3E;AAAA,MACJ;AAEA,aAAO,EAAE,QAAQ,UAAU,KAAK;AAAA,IACpC;AAAA,EACJ;;;ACrIO,MAAM,eAAe;AAAA;AAAA,IAExB,MAAM;AAAA;AAAA,IAEN,gBAAgB;AAAA;AAAA,IAEhB,UAAU;AAAA;AAAA,IAEV,OAAO;AAAA,EACX;AAuBO,MAAM,sBAAN,MAA0B;AAAA,IACrB,SAA4B,aAAa;AAAA,IAChC;AAAA,IACT,aAAgC;AAAA,IAExC,YAAY,WAA8B;AACtC,WAAK,aAAa;AAAA,IACtB;AAAA,IAEA,IAAI,QAA2B;AAC3B,aAAO,KAAK;AAAA,IAChB;AAAA,IAEA,IAAI,YAA+B;AAC/B,aAAO,KAAK;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAaA,UAAU,QAAmC;AAEzC,UAAI,KAAK,eAAe,QAAQ,KAAK,aAAa,KAAK,YAAY,MAAM,GAAG;AACxE,eAAO,EAAE,QAAQ,QAAQ,QAAQ,mBAAmB;AAAA,MACxD;AAEA,WAAK,aAAa;AAElB,UAAI,KAAK,eAAe,SAAS;AAE7B,aAAK,SAAS,aAAa;AAAA,MAC/B,OAAO;AACH,aAAK,SAAS,aAAa;AAAA,MAC/B;AACA,aAAO,EAAE,QAAQ,aAAa,OAAO;AAAA,IACzC;AAAA;AAAA,IAGQ,aAAa,GAAe,GAAwB;AACxD,UAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,eAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAC/B,YAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,QAAO;AAAA,MAC9B;AACA,aAAO;AAAA,IACX;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,kBACI,OACA,SACa;AACb,UAAI,KAAK,WAAW,aAAa,QAAQ,KAAK,WAAW,aAAa,OAAO;AACzE,eAAO,EAAE,QAAQ,QAAQ,QAAQ,cAAc,KAAK,MAAM,GAAG;AAAA,MACjE;AAEA,UAAI,KAAK,WAAW,aAAa,gBAAgB;AAE7C,cAAM,aAAa,SAAS,gBAAgB;AAC5C,YAAI,CAAC,YAAY;AACb,iBAAO,EAAE,QAAQ,QAAQ,QAAQ,uBAAuB;AAAA,QAC5D;AACA,aAAK,SAAS,aAAa;AAC3B,eAAO,EAAE,QAAQ,UAAU,MAAM;AAAA,MACrC;AAGA,aAAO,EAAE,QAAQ,UAAU,MAAM;AAAA,IACrC;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,kBAAkB,OAAsC;AACpD,UAAI,KAAK,WAAW,aAAa,QAAQ,KAAK,WAAW,aAAa,OAAO;AACzE,eAAO,EAAE,QAAQ,QAAQ,QAAQ,cAAc,KAAK,MAAM,GAAG;AAAA,MACjE;AAGA,WAAK,SAAS,aAAa;AAC3B,aAAO,EAAE,QAAQ,UAAU,MAAM;AAAA,IACrC;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,YAA2B;AACvB,UAAI,KAAK,eAAe,SAAS;AAE7B,eAAO,EAAE,QAAQ,SAAS,QAAQ,+CAA+C;AAAA,MACrF;AACA,WAAK,SAAS,aAAa;AAC3B,aAAO,EAAE,QAAQ,SAAS,QAAQ,eAAe;AAAA,IACrD;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,mBAAkC;AAC9B,WAAK,SAAS,aAAa;AAC3B,aAAO,EAAE,QAAQ,QAAQ;AAAA,IAC7B;AAAA,EACJ;;;ACvIA,MAAM,4BAA4B;AAiBlC,MAAM,qBAAsC,EAAE,KAAK,MAAM,KAAK,IAAI,IAAI,IAAK;AA8BpE,MAAM,iBAAN,MAAqB;AAAA,IAChB;AAAA,IACS;AAAA,IACA;AAAA;AAAA,IAGT;AAAA;AAAA,IAEA;AAAA;AAAA,IAGA,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAalB,qBAAqB;AAAA;AAAA,IAGZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IAET,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,iBAAiB;AAAA,IAEzB,YAAY,QAA8B;AACtC,WAAK,oBAAoB,OAAO;AAChC,WAAK,kBAAkB,OAAO,mBAAmB;AACjD,WAAK,QAAQ,OAAO;AAGpB,WAAK,YAAY,OAAO,aAAa;AACrC,WAAK,kBAAkB,OAAO;AAC9B,WAAK,iBAAiB,OAAO,kBAAkB;AAC/C,WAAK,qBAAqB,OAAO,sBAAsB;AACvD,WAAK,oBAAoB,OAAO,qBAAqB;AAAA,IACzD;AAAA;AAAA,IAGA,IAAI,eAAwB;AACxB,aAAO,KAAK,oBAAoB;AAAA,IACpC;AAAA;AAAA,IAGA,IAAI,iBAAyB;AACzB,aAAO,KAAK;AAAA,IAChB;AAAA;AAAA,IAGA,IAAI,mBAA2B;AAAE,aAAO,KAAK;AAAA,IAAmB;AAAA,IAChE,IAAI,iBAAiB,OAAe;AAAE,WAAK,oBAAoB;AAAA,IAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUtE,IAAI,4BAAqC;AACrC,aAAO,KAAK,IAAI,KAAK,eAAe,KAAK,KAAK;AAAA,IAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,IAAI,cAAuB;AAGvB,UAAI,KAAK,qBAAqB,EAAG,QAAO;AACxC,aAAO,KAAK,IAAI,KAAK,eAAe,KAAK,KAAK;AAAA,IAClD;AAAA;AAAA,IAGA,IAAI,gBAAyB;AACzB,aAAO,KAAK;AAAA,IAChB;AAAA;AAAA,IAGA,IAAI,cAAsB;AACtB,aAAO,KAAK;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,IAAI,YAAoB;AACpB,aAAO,KAAK;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,kBAAkB,oBAAkC;AAChD,WAAK,kBAAkB,KAAK,MAAM,IAAI;AACtC,WAAK,oBAAoB;AACzB,WAAK,kBAAkB;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,kBAAkB,oBAAkC;AAChD,UAAI,KAAK,aAAc;AACvB,WAAK,kBAAkB,KAAK,MAAM,IAAI;AACtC,WAAK,oBAAoB;AACzB,WAAK,kBAAkB;AAAA,IAC3B;AAAA;AAAA,IAGA,IAAI,oBAA4B;AAC5B,aAAO,KAAK;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAgBA,YAAY,wBAAgC,YAAoB,MAAc;AAC1E,UAAI,KAAK,oBAAoB,UAAa,KAAK,sBAAsB,OAAW;AAEhF,YAAM,oBAAoB,KAAK,kBAC3B,OAAO,yBAAyB,KAAK,iBAAiB;AAC1D,YAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,YAAM,QAAQ,QAAQ,YAAY;AAElC,UAAI,QAAQ,GAAG;AACX,aAAK,qBAAqB;AAAA,MAC9B;AAAA,IACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,uBAAuB,oBAAiD;AACpE,YAAM,SAAS,KAAK,kBAAkB,kBAAkB;AACxD,UAAI,WAAW,KAAM,QAAO;AAE5B,UAAI,KAAK,qBAAqB,GAAG;AAE7B,cAAM,uBAAuB,OAAO,eAAe,KAAK;AACxD,cAAM,QAAQ,KAAK,MAAM,IAAI;AAI7B,YAAI,OAAO,eAAe,OAAO;AAC7B,eAAK,qBAAqB;AAC1B,iBAAO;AAAA,QACX;AAEA,cAAM,WAAW,uBAAuB;AACxC,eAAO;AAAA,UACH,cAAc;AAAA,UACd;AAAA,UACA,YAAY,WAAW,CAAC,KAAK;AAAA,QACjC;AAAA,MACJ;AAEA,aAAO;AAAA,IACX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,uBAAuB,oBAAiD;AACpE,aAAO,KAAK,kBAAkB,kBAAkB;AAAA,IACpD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAkBA,yBAAyB,aAAqB,UAAwB;AAClE,WAAK,kBAAkB,WAAW;AAAA,IACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,uBAAuB,oBAA4B,eAA6B;AAC5E,UAAI,KAAK,oBAAoB,UAAa,KAAK,sBAAsB,OAAW;AAEhF,YAAM,mBAAmB,KAAK,kBAC1B,OAAO,qBAAqB,KAAK,iBAAiB;AAEtD,WAAK,kBAAkB,gBAAgB;AAAA,IAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAkBA,eAAe,oBAA4B,aAAsC;AAC7E,UAAI,uBAAuB,MAAM,gBAAgB,MAAO,QAAO;AAC/D,aAAO,KAAK,UAAU,IAAI,IAAI,OAAO,kBAAkB;AAAA,IAC3D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAcA,gBAAgB,oBAA4B,aAA4C;AAEpF,YAAM,YAAY,KAAK,eAAe,oBAAoB,WAAW;AACrE,UAAI,cAAc,KAAM,QAAO;AAE/B,WAAK,iBAAiB;AAGtB,UAAI,KAAK,oBAAoB,UAAa,KAAK,kBAAkB,EAAK,QAAO;AAE7E,YAAM,YAAY,YAAY;AAC9B,YAAM,WAAW,KAAK;AAGtB,UAAI,CAAC,KAAK,kBAAkB,YAAY,WAAW,KAAK,oBAAoB;AACxE,aAAK,iBAAiB;AAAA,MAC1B,WAAW,KAAK,kBAAkB,aAAa,WAAW,KAAK,mBAAmB;AAC9E,aAAK,iBAAiB;AACtB,aAAK,eAAe;AACpB,eAAO,EAAE,QAAQ,OAAO,aAAa,GAAK,WAAW,SAAS;AAAA,MAClE;AAEA,UAAI,KAAK,gBAAgB;AAErB,cAAM,YAAY,YAAY;AAC9B,cAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,YAAY,KAAK,oBAAoB,CAAC,GAAG,CAAC;AAC1E,cAAM,UAAU,KAAO,KAAK,iBAAiB,KAAO;AAEpD,cAAM,YAAY,KAAK,MAAM,UAAU,EAAE,IAAI;AAE7C,aAAK,eAAe,KAAK,IAAI,KAAK,IAAI,WAAW,CAAG,GAAG,KAAK,cAAc;AAAA,MAC9E;AAEA,aAAO;AAAA,QACH,QAAQ,KAAK;AAAA,QACb,aAAa,KAAK;AAAA,QAClB;AAAA,QACA;AAAA,MACJ;AAAA,IACJ;AAAA;AAAA;AAAA;AAAA,IAKA,QAAc;AACV,WAAK,kBAAkB;AACvB,WAAK,oBAAoB;AACzB,WAAK,kBAAkB;AACvB,WAAK,qBAAqB;AAC1B,WAAK,iBAAiB;AACtB,WAAK,eAAe;AAAA,IACxB;AAAA;AAAA,IAIQ,kBAAkB,oBAAiD;AACvE,UAAI,KAAK,oBAAoB,UAAa,KAAK,sBAAsB,QAAW;AAC5E,eAAO;AAAA,MACX;AAEA,YAAM,MAAM,KAAK,MAAM,IAAI;AAE3B,YAAM,eAAe,KAAK,kBACtB,OAAO,qBAAqB,KAAK,iBAAiB;AAEtD,YAAM,WAAW,eAAe;AAChC,YAAM,aAAa,WAAW,CAAC,KAAK;AAEpC,aAAO,EAAE,cAAc,UAAU,WAAW;AAAA,IAChD;AAAA,EACJ;;;ACvUO,MAAM,4BAAN,MAA8D;AAAA,IAChD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGT,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA,IAMlB,0BAA0B;AAAA;AAAA,IAG1B,mBAAmB;AAAA;AAAA,IAGV,gBAA0B,CAAC;AAAA,IAE5C,YAAY,QAAgC;AACxC,WAAK,yBAAyB,QAAQ,0BAA0B;AAChE,WAAK,kBAAkB,QAAQ,mBAAmB;AAClD,WAAK,oBAAoB,QAAQ,+BAA+B;AAChE,WAAK,oBAAoB,QAAQ,8BAA8B,OAAU;AACzE,WAAK,mBAAmB,QAAQ,oBAAoB;AACpD,WAAK,QAAQ;AAAA,IACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,SAAS,OAAgC;AACrC,MAAC,KAAwD,QAAQ;AAAA,IACrE;AAAA,IAEA,SAAS,SAA0C;AAC/C,cAAQ,QAAQ,MAAM;AAAA,QAClB,KAAK;AACD,iBAAO,KAAK,UAAU;AAAA,QAE1B,KAAK;AACD,iBAAO,KAAK,YAAY,QAAQ,UAAU;AAAA,QAE9C,KAAK;AACD,iBAAO,KAAK,eAAe,wBAAwB;AAAA,QAEvD,KAAK;AACD,iBAAO,KAAK,eAAe,gCAAgC;AAAA,QAE/D,KAAK;AACD,iBAAO,EAAE,MAAM,cAAc;AAAA,QAEjC,KAAK;AACD,iBAAO,KAAK,kBAAkB,QAAQ,OAAO;AAAA,MACrD;AAAA,IACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,gBAAsB;AAClB,WAAK,kBAAkB;AACvB,WAAK,0BAA0B;AAAA,IACnC;AAAA;AAAA,IAGA,QAAc;AACV,WAAK,kBAAkB;AACvB,WAAK,0BAA0B;AAC/B,WAAK,mBAAmB;AACxB,WAAK,cAAc,SAAS;AAAA,IAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASQ,eAAe,QAAgC;AACnD,UAAI,KAAK,yBAAyB;AAC9B,eAAO,EAAE,MAAM,eAAe;AAAA,MAClC;AACA,WAAK,0BAA0B;AAC/B,aAAO,EAAE,MAAM,kBAAkB,OAAO;AAAA,IAC5C;AAAA,IAEQ,YAA4B;AAChC,WAAK;AAGL,UAAI,KAAK,mBAAmB,KAAK,wBAAwB;AACrD,aAAK,kBAAkB;AACvB,aAAK,cAAc,SAAS;AAC5B,eAAO,KAAK,eAAe,wBAAwB;AAAA,MACvD;AAKA,UAAI,KAAK,OAAO;AACZ,cAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,aAAK,cAAc,KAAK,GAAG;AAE3B,cAAM,SAAS,MAAM,KAAK;AAC1B,eAAO,KAAK,cAAc,SAAS,KAAK,KAAK,cAAc,CAAC,IAAK,QAAQ;AACrE,eAAK,cAAc,MAAM;AAAA,QAC7B;AACA,YAAI,KAAK,cAAc,UAAU,KAAK,mBAAmB;AACrD,eAAK,cAAc,SAAS;AAC5B,eAAK,kBAAkB;AACvB,iBAAO,KAAK,eAAe,+BAA+B;AAAA,QAC9D;AAAA,MACJ;AAEA,aAAO,EAAE,MAAM,eAAe;AAAA,IAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOQ,YAAY,YAAoC;AACpD,UAAI,aAAa,KAAK,kBAAkB;AACpC,eAAO,EAAE,MAAM,eAAe;AAAA,MAClC;AAGA,UAAI,KAAK,OAAO;AACZ,cAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,aAAK,cAAc,KAAK,GAAG;AAC3B,cAAM,SAAS,MAAM,KAAK;AAC1B,eAAO,KAAK,cAAc,SAAS,KAAK,KAAK,cAAc,CAAC,IAAK,QAAQ;AACrE,eAAK,cAAc,MAAM;AAAA,QAC7B;AACA,YAAI,KAAK,cAAc,UAAU,KAAK,mBAAmB;AACrD,eAAK,cAAc,SAAS;AAC5B,iBAAO,KAAK,eAAe,gCAAgC;AAAA,QAC/D;AAAA,MACJ;AACA,aAAO,EAAE,MAAM,eAAe;AAAA,IAClC;AAAA,IAEQ,kBAAkB,SAAiC;AACvD,WAAK;AACL,UAAI,KAAK,oBAAoB,KAAK,iBAAiB;AAC/C,eAAO,EAAE,MAAM,aAAa,QAAQ,0BAA0B,OAAO,GAAG;AAAA,MAC5E;AACA,aAAO,EAAE,MAAM,cAAc;AAAA,IACjC;AAAA,EACJ;;;AC7PO,MAAM,gBAAgB;AAAA;AAAA,IAE3B,QAAQ;AAAA;AAAA,IAER,YAAY;AAAA;AAAA,IAEZ,cAAc;AAAA,EAChB;AAyCO,MAAM,2BAA4C;AAAA,IACvD,UAAU;AAAA;AAAA,IACV,eAAe;AAAA;AAAA,IACf,mBAAmB;AAAA;AAAA,IACnB,kBAAkB;AAAA;AAAA,IAElB,iBAAiB;AAAA;AAAA,IACjB,iBAAiB;AAAA;AAAA,IAEjB,qBAAqB;AAAA,IACrB,0BAA0B;AAAA,IAE1B,kBAAkB;AAAA;AAAA,IAClB,oBAAoB;AAAA;AAAA,IAEpB,iBAAiB;AAAA;AAAA,IACjB,oBAAoB;AAAA;AAAA,IACpB,wBAAwB;AAAA,IACxB,mBAAmB;AAAA,IAEnB,wBAAwB;AAAA;AAAA,IACxB,uBAAuB;AAAA,IAEvB,qBAAqB;AAAA;AAAA,IACrB,yBAAyB;AAAA,IACzB,aAAa;AAAA;AAAA,EACf;AASA,MAAM,eAA2B,EAAE,YAAY,MAAM;AACrD,MAAM,YAAwB,EAAE,YAAY,KAAK;AAQjD,MAAM,aAAN,MAAiB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACT,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IAEhB,YAAY,UAAkB;AAC5B,WAAK,WAAW;AAChB,WAAK,MAAM,IAAI,aAAa,QAAQ;AACpC,WAAK,MAAM,IAAI,YAAY,QAAQ;AAAA,IACrC;AAAA,IAEA,KAAK,OAAqB;AACxB,YAAM,MAAM,KAAK;AACjB,YAAM,OAAO,KAAK,WAAW;AAG7B,UAAI,KAAK,UAAU,OAAO,KAAK,UAAU,KAAK,SACvC,KAAK,IAAI,KAAK,QAAQ,GAAG,MAAM,MAAM;AAC1C,aAAK;AAAA,MACP;AAGA,aAAO,KAAK,UAAU,KAAK,SACjB,KAAK,IAAI,KAAK,KAAK,KAAK,QAAQ,KAAK,GAAG,IAAK,GAAG,KAAM,OAAO;AACrE,aAAK;AAAA,MACP;AAEA,WAAK,IAAI,KAAK,QAAQ,GAAG,IAAI;AAC7B,WAAK;AACL,WAAK,IAAI,IAAI,IAAI;AACjB,WAAK;AACL,UAAI,KAAK,QAAQ,IAAK,MAAK;AAAA,IAC7B;AAAA,IAEA,IAAI,MAAc;AAChB,UAAI,KAAK,UAAU,KAAK,MAAO,QAAO;AACtC,aAAO,KAAK,IAAI,KAAK,IAAI,KAAK,QAAQ,KAAK,QAAQ,IAAK,KAAK,QAAQ;AAAA,IACvE;AAAA,IAEA,QAAc;AACZ,WAAK,WAAW;AAChB,WAAK,QAAQ;AACb,WAAK,QAAQ;AACb,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAUO,MAAM,8BAAN,MAAkC;AAAA,IACtB;AAAA;AAAA,IAGT,aAAa;AAAA,IACb,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,kBAAkB;AAAA,IACT;AAAA;AAAA,IAGT,SAA6B,cAAc;AAAA,IAC3C,kBAAkB;AAAA,IAClB,mBAAmB;AAAA,IACnB,cAAc;AAAA,IACd,qBAAqB;AAAA;AAAA,IAGrB,kBAA4B,CAAC;AAAA;AAAA,IAG7B,mBAAmB;AAAA,IACnB,sBAAsB;AAAA;AAAA,IAGtB,uBAAuB;AAAA;AAAA,IAGvB,qBAAqB;AAAA;AAAA,IAGrB,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,oBAAoB;AAAA,IAE5B,YAAY,QAAyB;AACnC,WAAK,MAAM;AACX,WAAK,cAAc,IAAI,WAAW,OAAO,iBAAiB;AAAA,IAC5D;AAAA;AAAA,IAIA,IAAI,QAA4B;AAAE,aAAO,KAAK;AAAA,IAAQ;AAAA,IACtD,IAAI,cAAsB;AAAE,aAAO,KAAK;AAAA,IAAY;AAAA,IACpD,IAAI,cAAsB;AAAE,aAAO,KAAK;AAAA,IAAY;AAAA,IACpD,IAAI,eAAuB;AAAE,aAAO,KAAK,YAAY;AAAA,IAAK;AAAA,IAC1D,IAAI,aAAqB;AAAE,aAAO,KAAK;AAAA,IAAa;AAAA,IACpD,IAAI,sBAA+B;AAAE,aAAO,KAAK;AAAA,IAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQvE,IAAI,wBAAgC;AAClC,YAAM,WAAW,KAAK;AAAA,QACpB,KAAK,IAAI;AAAA,QACT,KAAK,aAAa,KAAK,IAAI,mBAAmB,KAAK;AAAA,QACnD,KAAK,YAAY;AAAA,MACnB;AACA,YAAM,YAAY,WAAW,KAAK;AAClC,UAAI,SAAS,KAAK,IAAI,WAAW,KAAK,IAAI,eAAe;AAEzD,UAAI,KAAK,qBAAqB,GAAG;AAC/B,iBAAS,KAAK,IAAI,QAAQ,KAAK,kBAAkB;AAAA,MACnD;AAEA,aAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,IAAI,4BAAoC;AACtC,UAAI,CAAC,KAAK,kBAAkB,KAAK,sBAAsB,GAAG;AACxD,eAAO,KAAK,IAAI;AAAA,MAClB;AACA,aAAO,KAAK;AAAA,QACV,KAAK,IAAI;AAAA,QACT,KAAK,oBAAoB,KAAK,IAAI;AAAA,MACpC;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAWA,eAAe,OAAe,WAAmB,WAAyB;AAExE,YAAM,YAAY,YAAY,YAAY;AAE1C,UAAI,KAAK,iBAAiB;AACxB,cAAM,IAAI,YAAY,KAAK;AAC3B,cAAM,OAAO,IAAI,IAAI,CAAC,IAAI;AAG1B,cAAM,SAAS,OAAO,KAAK;AAC3B,aAAK,cAAc,KAAK,IAAI,WAAW;AACvC,aAAK,cAAc,KAAK,IAAI,iBAAiB,OAAO,KAAK;AAGzD,aAAK,YAAY,KAAK,IAAI;AAAA,MAC5B;AAEA,WAAK,iBAAiB;AACtB,WAAK,kBAAkB;AAGvB,UAAI,KAAK,gBAAgB;AACvB,cAAM,oBAAoB,YAAY,KAAK;AAE3C,cAAM,kBAAkB,KAAK,IAAI,iBAAiB,IAAI;AAEtD,YAAI,KAAK,sBAAsB,GAAG;AAChC,eAAK,oBAAoB;AAAA,QAC3B,OAAO;AACL,eAAK,qBAAqB,KAAK,IAAI,YAAY,kBAAkB,KAAK;AAAA,QACxE;AAAA,MACF;AACA,WAAK,iBAAiB;AACtB,WAAK,iBAAiB;AAGtB,WAAK;AACL,UAAI,KAAK,qBAAqB,GAAG;AAC/B,aAAK,sBAAsB;AAAA,MAC7B;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,cAAc,OAAe,UAAwB;AACnD,WAAK,mBAAmB;AAExB,cAAQ,KAAK,QAAQ;AAAA,QACnB,KAAK,cAAc;AACjB,eAAK,SAAS,cAAc;AAC5B,eAAK,kBAAkB;AACvB,eAAK,mBAAmB;AACxB;AAAA,QAEF,KAAK,cAAc;AACjB,eAAK;AACL,cAAI,KAAK,oBAAoB,KACtB,QAAQ,KAAK,mBAAmB,KAAK,IAAI,iBAAiB;AAE/D,iBAAK,SAAS,cAAc;AAC5B,iBAAK,kBAAkB;AAAA,UACzB;AACA;AAAA,QAEF,KAAK,cAAc;AAEjB;AAAA,MACJ;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,KAAK,OAA2B;AAC9B,cAAQ,KAAK,QAAQ;AAAA,QACnB,KAAK,cAAc,YAAY;AAE7B,cAAI,QAAQ,KAAK,kBAAkB,KAAK,IAAI,iBAAiB;AAE3D,gBAAI,KAAK,oBAAoB,KAAK,IAAI,sBAC/B,QAAQ,KAAK,uBAAuB,KAAK,IAAI,wBAAwB;AAC1E,mBAAK,SAAS,cAAc;AAC5B,qBAAO;AAAA,YACT;AAAA,UACF;AACA;AAAA,QACF;AAAA,QAEA,KAAK,cAAc,cAAc;AAC/B,cAAI,QAAQ,KAAK,mBAAmB,KAAK,IAAI,mBAAmB;AAE9D,iBAAK;AACL,iBAAK,qBAAqB,KAAK;AAAA,cAC7B,KAAK,cAAc,KAAK,IAAI;AAAA,cAC5B,KAAK,IAAI,qBAAqB,KAAK,IAAI;AAAA,YACzC;AAGA,iBAAK,gBAAgB,KAAK,KAAK;AAC/B,iBAAK,qBAAqB,KAAK;AAC/B,gBAAI,KAAK,gBAAgB,UAAU,KAAK,IAAI,wBAAwB;AAClE,mBAAK,uBAAuB;AAAA,YAC9B;AAEA,iBAAK,SAAS,cAAc;AAC5B,iBAAK,mBAAmB;AACxB,mBAAO;AAAA,UACT;AACA;AAAA,QACF;AAAA,QAEA,KAAK,cAAc,QAAQ;AAEzB,cAAI,KAAK,qBAAqB,KACvB,KAAK,oBAAoB,KAAK,IAAI,uBAClC,KAAK,sBAAsB,KAC3B,QAAQ,KAAK,uBAAuB,KAAK,IAAI,yBAAyB;AAC3E,iBAAK,sBAAsB,KAAK,IAAI;AACpC,gBAAI,KAAK,qBAAqB,GAAG;AAC/B,mBAAK,qBAAqB;AAC1B,mBAAK,cAAc;AAAA,YACrB;AAEA,iBAAK,mBAAmB;AACxB,iBAAK,sBAAsB;AAAA,UAC7B;AAGA,cAAI,KAAK,sBAAsB;AAC7B,iBAAK,qBAAqB,KAAK;AAC/B,gBAAI,KAAK,gBAAgB,SAAS,KAAK,IAAI,wBAAwB;AACjE,mBAAK,uBAAuB;AAAA,YAC9B;AAAA,UACF;AACA;AAAA,QACF;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,mBAAmB,WAAyB;AAC1C,WAAK,qBAAqB;AAAA,IAC5B;AAAA;AAAA,IAGA,QAAc;AACZ,WAAK,aAAa;AAClB,WAAK,aAAa;AAClB,WAAK,iBAAiB;AACtB,WAAK,kBAAkB;AACvB,WAAK,YAAY,MAAM;AAEvB,WAAK,SAAS,cAAc;AAC5B,WAAK,kBAAkB;AACvB,WAAK,mBAAmB;AACxB,WAAK,cAAc;AACnB,WAAK,qBAAqB;AAE1B,WAAK,kBAAkB,CAAC;AACxB,WAAK,mBAAmB;AACxB,WAAK,sBAAsB;AAC3B,WAAK,uBAAuB;AAC5B,WAAK,qBAAqB;AAE1B,WAAK,iBAAiB;AACtB,WAAK,iBAAiB;AACtB,WAAK,oBAAoB;AAAA,IAC3B;AAAA;AAAA,IAIQ,qBAAqB,OAAqB;AAChD,YAAM,cAAc,QAAQ,KAAK,IAAI;AACrC,WAAK,kBAAkB,KAAK,gBAAgB,OAAO,OAAK,KAAK,WAAW;AAAA,IAC1E;AAAA,EACF;;;ACnaO,MAAM,kBAAN,MAAsB;AAAA,IACV;AAAA,IACA;AAAA;AAAA,IAGT,aAA4B;AAAA;AAAA,IAE5B,YAA2B;AAAA;AAAA,IAG3B,gBAAwB;AAAA,IAEhC,YAAY,OAAoB,SAAkC;AAChE,WAAK,QAAQ;AACb,WAAK,YAAY,SAAS,aAAa;AAAA,IACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,SAAS,oBAAoC;AAC3C,YAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,YAAM,QAAQ,OAAO,kBAAkB;AAMvC,UAAI,KAAK,eAAe,QAAQ,KAAK,cAAc,MAAM;AAEvD,aAAK,aAAa,QAAQ,KAAK;AAC/B,aAAK,YAAY;AACjB,aAAK,gBAAgB;AACrB,eAAO,KAAK;AAAA,MACd;AAGA,YAAM,WAAW,QAAQ,KAAK;AAC9B,UAAI,eAAe,KAAK,aAAa;AAOrC,YAAM,eAAe,QAAQ,KAAK;AAElC,UAAI,eAAe,SAAS,eAAe,KAAQ;AAGjD,aAAK,aAAa,QAAQ,KAAK;AAC/B,aAAK,YAAY;AACjB,uBAAe,KAAK;AAAA,MACtB;AAEA,WAAK,gBAAgB;AACrB,aAAO;AAAA,IACT;AAAA;AAAA,IAGA,QAAc;AACZ,WAAK,aAAa;AAClB,WAAK,YAAY;AACjB,WAAK,gBAAgB;AAAA,IACvB;AAAA,EACF;;;ACnEO,WAAS,gBAAgB,gBAAuC;AACrE,QAAI,iBAAiB,GAAG;AACtB,aAAO,EAAE,WAAW,GAAG,WAAW,GAAG;AAAA,IACvC,WAAW,iBAAiB,GAAG;AAC7B,aAAO,EAAE,WAAW,GAAG,WAAW,IAAI;AAAA,IACxC,WAAW,iBAAiB,IAAI;AAC9B,aAAO,EAAE,WAAW,GAAG,WAAW,IAAI;AAAA,IACxC,OAAO;AACL,aAAO,EAAE,WAAW,IAAI,WAAW,IAAI;AAAA,IACzC;AAAA,EACF;AAiBO,MAAM,gBAAN,MAAoB;AAAA,IACR;AAAA,IACA;AAAA,IAET,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,eAAe;AAAA,IACf,YAAY;AAAA,IAEpB,YAAY,QAAuB;AACjC,WAAK,YAAY,OAAO;AACxB,WAAK,YAAY,OAAO;AAAA,IAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,YAAY,OAAqB;AAC/B,WAAK;AACL,UAAI,KAAK,gBAAgB,GAAG;AAC1B,aAAK,gBAAgB;AAAA,MACvB;AACA,WAAK,eAAe;AAGpB,UAAI,CAAC,KAAK,WAAW;AACnB,cAAM,UAAU,KAAK,eAAe,KAAK;AACzC,YAAI,KAAK,eAAe,KAAK,aAAa,WAAW,KAAK,WAAW;AACnE,eAAK,YAAY;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA;AAAA,IAGA,IAAI,cAAuB;AACzB,aAAO,KAAK;AAAA,IACd;AAAA;AAAA,IAGA,IAAI,aAAqB;AACvB,aAAO,KAAK;AAAA,IACd;AAAA;AAAA,IAGA,IAAI,YAAoB;AACtB,UAAI,KAAK,gBAAgB,EAAG,QAAO;AACnC,aAAO,KAAK,eAAe,KAAK;AAAA,IAClC;AAAA;AAAA,IAGA,IAAI,iBAAyB;AAC3B,aAAO;AAAA,IACT;AAAA;AAAA,IAGA,QAAc;AACZ,WAAK,cAAc;AACnB,WAAK,gBAAgB;AACrB,WAAK,eAAe;AACpB,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;;;ACjGO,WAAS,kBAAkB,OAAe,SAAqC;AACpF,UAAM,YAAY,MAAM,MAAM,GAAG,EAAE,CAAC,EAAG,YAAY;AAEnD,YAAQ,WAAW;AAAA,MACjB,KAAK;AAAA,MACL,KAAK;AACH,eAAO,eAAe,OAAO;AAAA,MAC/B,KAAK;AAAA,MACL,KAAK;AACH,eAAO,eAAe,OAAO;AAAA,MAC/B,KAAK;AACH,eAAO,cAAc,OAAO;AAAA,MAC9B;AACE,eAAO;AAAA,IACX;AAAA,EACF;AA4BA,MAAM,uBAAuB;AAE7B,WAAS,eAAe,SAAqC;AAC3D,QAAI,QAAQ,SAAS,EAAG,QAAO;AAE/B,QAAI,MAAM;AACV,QAAI,aAAa;AACjB,WAAO,MAAM,IAAI,QAAQ,QAAQ;AAC/B,UAAI,EAAE,aAAa,qBAAsB,QAAO;AAGhD,YAAM,SAAU,QAAQ,GAAG,KAAM,KAAO,QAAQ,MAAM,CAAC,KAAM,KAC7C,QAAQ,MAAM,CAAC,KAAM,IAAK,QAAQ,MAAM,CAAC;AACzD,aAAO;AAEP,UAAI,UAAU,KAAK,OAAO,QAAQ,OAAQ;AAE1C,YAAM,UAAU,QAAQ,GAAG,IAAK;AAGhC,UAAI,YAAY,EAAG,QAAO;AAC1B,UAAI,YAAY,EAAG,QAAO;AAG1B,UAAI,YAAY,KAAK,YAAY,KAAK,YAAY,GAAG;AAEnD,eAAO;AACP;AAAA,MACF;AAGA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAWA,WAAS,eAAe,SAAqC;AAC3D,QAAI,QAAQ,SAAS,EAAG,QAAO;AAE/B,QAAI,MAAM;AACV,QAAI,aAAa;AACjB,WAAO,MAAM,IAAI,QAAQ,QAAQ;AAC/B,UAAI,EAAE,aAAa,qBAAsB,QAAO;AAEhD,YAAM,SAAU,QAAQ,GAAG,KAAM,KAAO,QAAQ,MAAM,CAAC,KAAM,KAC7C,QAAQ,MAAM,CAAC,KAAM,IAAK,QAAQ,MAAM,CAAC;AACzD,aAAO;AAEP,UAAI,UAAU,KAAK,MAAM,KAAK,QAAQ,OAAQ;AAE9C,YAAM,UAAW,QAAQ,GAAG,KAAM,IAAK;AAGvC,UAAI,YAAY,MAAM,YAAY,GAAI,QAAO;AAC7C,UAAI,YAAY,GAAI,QAAO;AAG3B,UAAI,YAAY,MAAM,YAAY,MAAM,YAAY,IAAI;AACtD,eAAO;AACP;AAAA,MACF;AAGA,UAAI,YAAY,MAAM,YAAY,MAAM,YAAY,IAAI;AACtD,eAAO;AACP;AAAA,MACF;AAGA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AAaA,WAAS,cAAc,SAAqC;AAC1D,QAAI,QAAQ,SAAS,EAAG,QAAO;AAE/B,UAAM,UAAW,QAAQ,CAAC,KAAM,IAAK;AAGrC,QAAI,YAAY,EAAG,QAAO;AAG1B,QAAI,YAAY,KAAK,QAAQ,UAAU,GAAG;AACxC,YAAM,cAAe,QAAQ,CAAC,KAAM,IAAK;AACzC,UAAI,gBAAgB,EAAG,QAAO;AAAA,IAChC;AAGA,QAAI,YAAY,KAAK,YAAY,EAAG,QAAO;AAE3C,WAAO;AAAA,EACT;;;AC9KO,MAAM,aAAa;AAepB,WAAU,OAAO,GAAkB;AACvC,QAAI,OAAO,MAAM,UAAU;AACzB,UAAI,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,GAAG;AACjC,cAAM,IAAI,WACR,oDAAoD,CAAC,EAAE;MAE3D;AACA,UAAI,IAAI,OAAO,kBAAkB;AAC/B,cAAM,IAAI,WACR,uBAAuB,CAAC,2DAA2D;MAEvF;AACA,aAAO,OAAO,CAAC;IACjB;AAEA,QAAI,IAAI,MAAM,IAAI,YAAY;AAC5B,YAAM,IAAI,WACR,0CAAgC,UAAU,SAAS,CAAC,EAAE;IAE1D;AACA,WAAO;EACT;;;ACeO,MAAM,gBAAgB;IAC3B,MAAM;IACN,qBAAqB;IACrB,2BAA2B;IAC3B,WAAW;IACX,qBAAqB;;AAIhB,MAAM,gBAAgB;IAC3B,GAAG;IACH,mBAAmB;IACnB,qBAAqB;;AAoBhB,MAAM,iBAAyE;IACpF,yBAAyB,EAAE,MAAM,GAAM,MAAM,SAAQ;IACrD,qBAAqB,EAAE,MAAM,GAAM,MAAM,QAAO;IAChD,oBAAoB,EAAE,MAAM,GAAM,MAAM,SAAQ;IAChD,2BAA2B,EAAE,MAAM,GAAM,MAAM,SAAQ;IACvD,SAAS,EAAE,MAAM,GAAM,MAAM,SAAQ;IACrC,gBAAgB,EAAE,MAAM,GAAM,MAAM,WAAU;IAC9C,cAAc,EAAE,MAAM,IAAM,MAAM,SAAQ;IAC1C,SAAS,EAAE,MAAM,IAAM,MAAM,QAAO;IACpC,qBAAqB,EAAE,MAAM,IAAM,MAAM,QAAO;IAChD,qBAAqB,EAAE,MAAM,IAAM,MAAM,QAAO;IAChD,aAAa,EAAE,MAAM,IAAM,MAAM,QAAO;IACxC,mBAAmB,EAAE,MAAM,IAAM,MAAM,SAAQ;IAC/C,wBAAwB,EAAE,MAAM,IAAM,MAAM,YAAW;;AASlD,MAAM,iBAAyE;IACpF,yBAAyB,EAAE,MAAM,GAAM,MAAM,SAAQ;IACrD,qBAAqB,EAAE,MAAM,GAAM,MAAM,QAAO;IAChD,oBAAoB,EAAE,MAAM,GAAM,MAAM,SAAQ;IAChD,2BAA2B,EAAE,MAAM,GAAM,MAAM,SAAQ;IACvD,SAAS,EAAE,MAAM,GAAM,MAAM,SAAQ;IACrC,gBAAgB,EAAE,MAAM,GAAM,MAAM,WAAU;IAC9C,cAAc,EAAE,MAAM,IAAM,MAAM,SAAQ;IAC1C,SAAS,EAAE,MAAM,IAAM,MAAM,QAAO;IACpC,qBAAqB,EAAE,MAAM,IAAM,MAAM,QAAO;IAChD,iBAAiB,EAAE,MAAM,IAAM,MAAM,iBAAgB;IACrD,aAAa,EAAE,MAAM,IAAM,MAAM,QAAO;IACxC,iBAAiB,EAAE,MAAM,IAAM,MAAM,QAAO;IAC5C,iBAAiB,EAAE,MAAM,IAAM,MAAM,QAAO;IAC5C,iBAAiB,EAAE,MAAM,IAAM,MAAM,QAAO;IAC5C,iBAAiB,EAAE,MAAM,IAAM,MAAM,QAAO;IAC5C,wBAAwB,EAAE,MAAM,IAAM,MAAM,QAAO;IACnD,uBAAuB,EAAE,MAAM,IAAM,MAAM,QAAO;IAClD,mBAAmB,EAAE,MAAM,IAAM,MAAM,SAAQ;IAC/C,wBAAwB,EAAE,MAAM,IAAM,MAAM,YAAW;IACvD,oBAAoB,EAAE,MAAM,IAAM,MAAM,QAAO;;;;ACzH1C,MAAM,eAAe;IAC1B,UAAU,OAAO,CAAG;IACpB,gBAAgB,OAAO,CAAG;IAC1B,cAAc,OAAO,CAAG;IACxB,oBAAoB,OAAO,CAAG;IAC9B,oBAAoB,OAAO,CAAG;IAC9B,uBAAuB,OAAO,CAAG;IACjC,4BAA4B,OAAO,CAAG;IACtC,mBAAmB,OAAO,CAAG;IAC7B,cAAc,OAAO,CAAG;IACxB,gBAAgB,OAAO,CAAG;IAC1B,gBAAgB,OAAO,EAAI;IAC3B,yBAAyB,OAAO,EAAI;IACpC,qBAAqB,OAAO,EAAI;IAChC,2BAA2B,OAAO,EAAI;IACtC,4BAA4B,OAAO,EAAI;IACvC,4BAA4B,OAAO,EAAI;IACvC,sBAAsB,OAAO,EAAI;IACjC,0BAA0B,OAAO,EAAI;IACrC,oBAAoB,OAAO,EAAI;IAC/B,mBAAmB,OAAO,EAAI;IAC9B,qBAAqB,OAAO,EAAI;;IAEhC,0BAA0B,OAAO,EAAI;;AAOhC,MAAM,eAAe;IAC1B,gBAAgB,OAAO,CAAG;IAC1B,cAAc,OAAO,CAAG;IACxB,SAAS,OAAO,CAAG;IACnB,eAAe,OAAO,CAAG;IACzB,sBAAsB,OAAO,CAAG;IAChC,oBAAoB,OAAO,CAAG;IAC9B,gBAAgB,OAAO,EAAI;IAC3B,eAAe,OAAO,EAAI;IAC1B,iBAAiB,OAAO,EAAI;IAC5B,wBAAwB,OAAO,EAAI;IACnC,cAAc,OAAO,EAAI;IACzB,gBAAgB,OAAO,EAAI;IAC3B,qBAAqB,OAAO,EAAI;IAChC,4BAA4B,OAAO,EAAI;;IAEvC,uBAAuB,OAAO,EAAI;;IAElC,UAAU,OAAO,EAAI;;AAWhB,MAAM,kBAAkB;IAC7B,gBAAgB,OAAO,CAAG;IAC1B,cAAc,OAAO,CAAG;IACxB,aAAa,OAAO,CAAG;IACvB,oBAAoB,OAAO,CAAG;IAC9B,YAAY,OAAO,CAAG;IACtB,SAAS,OAAO,CAAG;IACnB,gBAAgB,OAAO,CAAG;IAC1B,eAAe,OAAO,CAAG;IACzB,iBAAiB,OAAO,EAAI;;AAWvB,MAAM,kBAAkB;IAC7B,gBAAgB,OAAO,CAAG;IAC1B,WAAW,OAAO,CAAG;IACrB,kBAAkB,OAAO,CAAG;IAC5B,gBAAgB,OAAO,CAAG;IAC1B,uBAAuB,OAAO,CAAG;IACjC,iBAAiB,OAAO,EAAI;;AAYvB,MAAM,iBAAiB;IAC5B,gBAAgB,OAAO,CAAG;IAC1B,cAAc,OAAO,CAAG;IACxB,SAAS,OAAO,CAAG;IACnB,eAAe,OAAO,CAAG;IACzB,sBAAsB,OAAO,CAAG;IAChC,oBAAoB,OAAO,CAAG;IAC9B,YAAY,OAAO,CAAG;IACtB,gBAAgB,OAAO,CAAG;IAC1B,gBAAgB,OAAO,EAAI;IAC3B,eAAe,OAAO,EAAI;IAC1B,iBAAiB,OAAO,EAAI;IAC5B,wBAAwB,OAAO,EAAI;IACnC,cAAc,OAAO,EAAI;IACzB,gBAAgB,OAAO,EAAI;IAC3B,qBAAqB,OAAO,EAAI;IAChC,4BAA4B,OAAO,EAAI;IACvC,uBAAuB,OAAO,EAAI;IAClC,UAAU,OAAO,EAAI;;AAOhB,MAAM,iBAAiB;IAC5B,gBAAgB,OAAO,CAAG;IAC1B,cAAc,OAAO,CAAG;IACxB,SAAS,OAAO,CAAG;IACnB,eAAe,OAAO,CAAG;IACzB,sBAAsB,OAAO,CAAG;IAChC,oBAAoB,OAAO,CAAG;IAC9B,YAAY,OAAO,CAAG;IACtB,gBAAgB,OAAO,CAAG;IAC1B,gBAAgB,OAAO,EAAI;IAC3B,eAAe,OAAO,EAAI;IAC1B,iBAAiB,OAAO,EAAI;IAC5B,cAAc,OAAO,EAAI;IACzB,gBAAgB,OAAO,EAAI;IAC3B,qBAAqB,OAAO,EAAI;IAChC,uBAAuB,OAAO,EAAI;IAClC,UAAU,OAAO,EAAI;IACrB,qBAAqB,OAAO,EAAI;IAChC,gBAAgB,OAAO,EAAI;;AAOtB,MAAM,oBAAoB;IAC/B,gBAAgB,OAAO,CAAG;IAC1B,cAAc,OAAO,CAAG;IACxB,aAAa,OAAO,CAAG;IACvB,oBAAoB,OAAO,CAAG;IAC9B,YAAY,OAAO,CAAG;IACtB,gBAAgB,OAAO,CAAG;IAC1B,SAAS,OAAO,CAAG;IACnB,eAAe,OAAO,CAAG;IACzB,gBAAgB,OAAO,CAAG;IAC1B,iBAAiB,OAAO,EAAI;;AASvB,MAAM,oBAAoB;IAC/B,gBAAgB,OAAO,CAAG;IAC1B,WAAW,OAAO,CAAG;IACrB,kBAAkB,OAAO,CAAG;IAC5B,gBAAgB,OAAO,CAAG;IAC1B,YAAY,OAAO,CAAG;IACtB,gBAAgB,OAAO,CAAG;IAC1B,uBAAuB,OAAO,CAAG;IACjC,oBAAoB,OAAO,CAAG;IAC9B,gBAAgB,OAAO,CAAG;IAC1B,iBAAiB,OAAO,EAAI;;;;ACE9B,MAAM,0BAA0B,IAAI,WAAW,CAAC,IAAM,KAAM,KAAM,KAAM,KAAM,KAAM,KAAM,GAAI,CAAC;;;AC1DxF,MAAM,iCAAuD,IAAI,IACtE,OAAO,OAAO,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,GAAG,EAAE,IAAI,CAAU,CAAC;AAItE,MAAM,iCAAuD,IAAI,IACtE,OAAO,OAAO,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,GAAG,EAAE,IAAI,CAAU,CAAC;;;AClI7E,MAAM,UAAU,IAAI,YAAW;AAC/B,MAAM,UAAU,IAAI,YAAW;;;ACGxB,MAAM,cAAc;IACzB,gBAAgB,OAAO,CAAI;IAC3B,WAAW,OAAO,CAAI;IACtB,cAAc,OAAO,CAAI;IACzB,eAAe,OAAO,CAAI;IAC1B,mBAAmB,OAAO,CAAI;IAC9B,YAAY,OAAO,CAAI;IACvB,WAAW,OAAO,CAAI;IACtB,wBAAwB,OAAO,CAAI;IACnC,aAAa,OAAO,EAAI;IACxB,cAAc,OAAO,EAAI;IACzB,0BAA0B,OAAO,EAAI;IACrC,cAAc,OAAO,EAAI;IACzB,gBAAgB,OAAO,EAAI;IAC3B,QAAQ,OAAO,EAAI;IACnB,qBAAqB,OAAO,EAAI;IAChC,gBAAgB,OAAO,EAAI;IAC3B,OAAO,OAAO,EAAI;IAClB,cAAc,OAAO,EAAI;IACzB,UAAU,OAAO,EAAI;IACrB,kBAAkB,OAAO,EAAI;IAC7B,SAAS,OAAO,EAAI;IACpB,YAAY,OAAO,EAAI;IACvB,cAAc,OAAO,EAAI;IACzB,cAAc,OAAO,EAAI;;;;ACxBpB,MAAM,aAAa;;IAExB,MAAM,OAAO,CAAI;;IAEjB,gBAAgB,OAAO,CAAI;;IAE3B,qBAAqB,OAAO,CAAI;;IAEhC,2BAA2B,OAAO,CAAI;;IAEtC,WAAW,OAAO,CAAI;;IAEtB,qBAAqB,OAAO,CAAI;;AAO3B,MAAM,eAAe;;IAE1B,kBAAkB,OAAO,CAAI;;;;;IAK7B,yBAAyB,OAAO,CAAI;;IAEpC,qBAAqB,OAAO,CAAI;;IAEhC,oBAAoB,OAAO,CAAI;;IAE/B,2BAA2B,OAAO,CAAI;;IAEtC,SAAS,OAAO,CAAI;;IAEpB,gBAAgB,OAAO,CAAI;;IAE3B,cAAc,OAAO,EAAI;;IAEzB,SAAS,OAAO,EAAI;;IAEpB,qBAAqB,OAAO,EAAI;;IAEhC,qBAAqB,OAAO,EAAI;;IAEhC,aAAa,OAAO,EAAI;;IAExB,mBAAmB,OAAO,EAAI;;;;;;;IAO9B,wBAAwB,OAAO,EAAI;;;;ACjCrC,MAAM,wBAAwB,oBAAI,IAAY;IAC5C,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;GACd;AAGD,MAAM,sBAAsB,oBAAI,IAAoB;IAClD,CAAC,aAAa,kBAA4B,kBAAkB;IAC5D,CAAC,aAAa,SAAmB,SAAS;IAC1C,CAAC,aAAa,gBAA0B,gBAAgB;IACxD,CAAC,aAAa,SAAmB,SAAS;IAC1C,CAAC,aAAa,qBAA+B,qBAAqB;IAClE,CAAC,aAAa,qBAA+B,qBAAqB;IAClE,CAAC,aAAa,aAAuB,aAAa;IAClD,CAAC,aAAa,mBAA6B,mBAAmB;GAC/D;;;AC6FD,MAAM,aAAa,oBAAI,IAAG;AAC1B,aAAW,CAAC,MAAM,IAAI,KAAK,OAAO,QAAQ,WAAW,GAAG;AACtD,eAAW,IAAI,MAAM,IAA8B;EACrD;;;ACrFA,MAAM,MAAM;;IAEV,cAA2B;IAC3B,WAA2B;IAC3B,kBAA2B;IAC3B,qBAA2B;IAC3B,uBAA2B;IAC3B,aAA2B;IAC3B,OAA2B;IAC3B,cAA2B;IAC3B,cAA2B;IAC3B,sBAA2B;IAC3B,yBAA2B;;IAG3B,cAA2B;IAC3B,cAA2B;IAC3B,iBAA2B;IAC3B,UAA2B;IAC3B,aAA2B;IAC3B,iBAA2B;IAC3B,oBAA2B;IAC3B,wBAA2B;IAC3B,2BAA2B;IAC3B,mBAA2B;IAC3B,wBAA2B;IAC3B,0BAA2B;IAC3B,cAA2B;IAC3B,QAA2B;IAC3B,gBAA2B;IAC3B,kBAA2B;;IAG3B,SAA2B;IAC3B,YAA2B;IAC3B,eAA2B;;AAI7B,MAAM,sBAAmC,oBAAI,IAAI,CAAC,IAAI,SAAS,IAAI,YAAY,IAAI,aAAa,CAAC;;;ACuDjG,MAAM,eAAe,IAAI,YAAW;AACpC,MAAM,eAAe,IAAI,YAAW;AA2CpC,MAAM,mBAAmB,oBAAI,IAAY;IACvC,OAAO,cAAc,IAAI;IACzB,OAAO,cAAc,SAAS;IAC9B,OAAO,cAAc,yBAAyB;IAC9C,OAAO,cAAc,mBAAmB;GACzC;;;ACrMM,MAAM,eAAe;;IAE1B,QAAQ,OAAO,CAAG;;;;;IAKlB,uBAAuB,OAAO,CAAG;;IAEjC,cAAc,OAAO,CAAG;;IAExB,cAAc,OAAO,CAAG;;;;ACmB1B,MAAM,yBAAyB,oBAAI,IAAI;IACrC,aAAa;IACb,aAAa;IACb,aAAa;GACd;AAED,MAAM,yBAAyB,oBAAI,IAAI;IACrC,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;GACd;;;AC+gBD,MAAM,kBAAkB,OAAO,OAAO,gBAAgB;;;AChjB/C,MAAM,YAAY;;IAEvB,QAAQ,OAAO,CAAG;;IAElB,UAAU,OAAO,CAAG;;IAEpB,WAAW,OAAO,CAAG;;IAErB,WAAW,OAAO,CAAG;;;;ACwEvB,MAAM,mCAAwD,oBAAI,IAAI;IACpE,cAAc;;IACd,cAAc;;IACd,cAAc;;IACd,cAAc;;GACf;;;ACND,MAAM,uBAAuB,oBAAI,IAAY;IAC3C,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;IACb,aAAa;GACd;AAOD,MAAM,gCAA0D,oBAAI,IAAI;;IAEtE,CAAC,aAAa,kBAA4B,oBAAI,IAAI,CAAC,cAAc,aAAa,gBAAgB,CAAC,CAAC;;;IAGhG,CAAC,aAAa,qBAA+B,oBAAI,IAAI;MACnD;MAAW;MAAa;MAAkB;MAC1C;MAAqB;MAAgB;KACtC,CAAC;;IAEF,CAAC,aAAa,SAAmB,oBAAI,IAAI,CAAC,gBAAgB,WAAW,YAAY,CAAC,CAAC;;IAEnF,CAAC,aAAa,gBAA0B,oBAAI,IAAI,CAAC,gBAAgB,WAAW,YAAY,CAAC,CAAC;;IAE1F,CAAC,aAAa,SAAmB,oBAAI,IAAI,CAAC,aAAa,kBAAkB,WAAW,cAAc,qBAAqB,CAAC,CAAC;;IAEzH,CAAC,aAAa,qBAA+B,oBAAI,IAAI,CAAC,aAAa,SAAS,kBAAkB,YAAY,CAAC,CAAC;;IAE5G,CAAC,aAAa,qBAA+B,oBAAI,IAAI,CAAC,aAAa,cAAc,gBAAgB,CAAC,CAAC;;;IAGnG,CAAC,aAAa,aAAuB,oBAAI,IAAI,CAAC,aAAa,cAAc,OAAO,CAAC,CAAC;;IAElF,CAAC,aAAa,mBAA6B,oBAAI,IAAI,CAAC,cAAc,aAAa,gBAAgB,CAAC,CAAC;;;IAGjG,CAAC,aAAa,wBAAkC,oBAAI,IAAI,CAAC,gBAAgB,CAAC,CAAC;GAC5E;AAQD,MAAM,0BAA0B,oBAAI,IAAY;IAC9C,aAAa;;IACb,aAAa;;IACb,aAAa;;IACb,aAAa;;IACb,aAAa;;IACb,aAAa;;IACb,aAAa;;IACb,aAAa;;IACb,aAAa;;IACb,aAAa;;IACb,aAAa;;IACb,aAAa;;IACb,aAAa;GACd;AAWD,MAAM,mCAA6D,oBAAI,IAAI;;IAEzE,CAAC,aAAa,yBAAmC,oBAAI,IAAI,CAAC,cAAc,aAAa,gBAAgB,CAAC,CAAC;;IAEvG,CAAC,aAAa,qBAA+B,oBAAI,IAAI;MACnD;MAAW;MAAa;MAAkB;MAAuB;MACjE;MAAqB;MAAgB;KACtC,CAAC;;IAEF,CAAC,aAAa,oBAA8B,oBAAI,IAAI,CAAC,WAAW,CAAC,CAAC;;IAElE,CAAC,aAAa,2BAAqC,oBAAI,IAAI,CAAC,cAAc,aAAa,gBAAgB,CAAC,CAAC;;IAEzG,CAAC,aAAa,SAAmB,oBAAI,IAAI,CAAC,gBAAgB,WAAW,cAAc,mBAAmB,CAAC,CAAC;;IAExG,CAAC,aAAa,gBAA0B,oBAAI,IAAI,CAAC,gBAAgB,WAAW,qBAAqB,iBAAiB,CAAC,CAAC;;IAEpH,CAAC,aAAa,cAAwB,oBAAI,IAAI,CAAC,OAAO,CAAC,CAAC;;IAExD,CAAC,aAAa,SAAmB,oBAAI,IAAI,CAAC,aAAa,kBAAkB,WAAW,cAAc,kBAAkB,CAAC,CAAC;;IAEtH,CAAC,aAAa,qBAA+B,oBAAI,IAAI,CAAC,aAAa,SAAS,kBAAkB,YAAY,CAAC,CAAC;;IAE5G,CAAC,aAAa,qBAA+B,oBAAI,IAAI,CAAC,aAAa,cAAc,gBAAgB,CAAC,CAAC;;IAEnG,CAAC,aAAa,aAAuB,oBAAI,IAAI,CAAC,aAAa,cAAc,OAAO,CAAC,CAAC;;IAElF,CAAC,aAAa,mBAA6B,oBAAI,IAAI,CAAC,cAAc,aAAa,gBAAgB,CAAC,CAAC;;;IAGjG,CAAC,aAAa,wBAAkC,oBAAI,IAAI,CAAC,gBAAgB,CAAC,CAAC;GAC5E;AAKD,MAAM,kBAAkB;AAGxB,MAAM,yBAAyB,oBAAI,IAAY,CAAC,OAAO,OAAO,OAAO,OAAO,KAAK,CAAC;AAGlF,MAAM,0BAA0B,oBAAI,IAAY;IAC9C,GAAG;IACH;IACA,GAAG;IACH;;GACD;AAUD,MAAM,mCAA6D,oBAAI,IAAI;IACzE,CAAC,aAAa,yBAAmC,oBAAI,IAAI,CAAC,aAAa,WAAW,kBAAkB,kBAAkB,CAAC,CAAC;IACxH,CAAC,aAAa,qBAA+B,oBAAI,IAAI;MACnD;MAAW;MAAa;MAAkB;MAAuB;MACjE;MAAqB;MAAgB;KACtC,CAAC;IACF,CAAC,aAAa,oBAA8B,oBAAI,IAAI,CAAC,aAAa,kBAAkB,CAAC,CAAC;IACtF,CAAC,aAAa,2BAAqC,oBAAI,IAAI,CAAC,aAAa,WAAW,kBAAkB,kBAAkB,CAAC,CAAC;IAC1H,CAAC,aAAa,SAAmB,oBAAI,IAAI;MAAC;MAAgB;MAAW;MAAc;MACjF;MAA0B;MAAuB;IAAsB,CAAC,CAAC;IAC3E,CAAC,aAAa,gBAA0B,oBAAI,IAAI;MAC9C;MAAgB;MAAW;MAAqB;MAAmB;KACpE,CAAC;IACF,CAAC,aAAa,cAAwB,oBAAI,IAAI,CAAC,OAAO,CAAC,CAAC;IACxD,CAAC,aAAa,SAAmB,oBAAI,IAAI,CAAC,aAAa,kBAAkB,WAAW,oBAAoB,sBAAsB,CAAC,CAAC;IAChI,CAAC,aAAa,qBAA+B,oBAAI,IAAI,CAAC,aAAa,SAAS,kBAAkB,WAAW,kBAAkB,CAAC,CAAC;IAC7H,CAAC,aAAa,qBAA+B,oBAAI,IAAI;MACnD;MAAa;MAAkB;MAAW;MAAS;MAAoB;KACxE,CAAC;IACF,CAAC,aAAa,aAAuB,oBAAI,IAAI,CAAC,aAAa,SAAS,WAAW,kBAAkB,CAAC,CAAC;IACnG,CAAC,iBAAiB,oBAAI,IAAI,CAAC,aAAa,kBAAkB,kBAAkB,CAAC,CAAC;IAC9E,CAAC,OAAO,oBAAI,IAAI,CAAC,aAAa,SAAS,kBAAkB,kBAAkB,CAAC,CAAC;IAC7E,CAAC,OAAO,oBAAI,IAAI,CAAC,aAAa,SAAS,kBAAkB,kBAAkB,CAAC,CAAC;IAC7E,CAAC,OAAO,oBAAI,IAAI,CAAC,aAAa,SAAS,kBAAkB,kBAAkB,CAAC,CAAC;IAC7E,CAAC,OAAO,oBAAI,IAAI,CAAC,aAAa,SAAS,kBAAkB,kBAAkB,CAAC,CAAC;IAC7E,CAAC,OAAO,oBAAI,IAAI,CAAC,kBAAkB,kBAAkB,CAAC,CAAC;IACvD,CAAC,aAAa,mBAA6B,oBAAI,IAAI,CAAC,aAAa,kBAAkB,kBAAkB,CAAC,CAAC;IACvG,CAAC,aAAa,wBAAkC,oBAAI,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC3E,CAAC,OAAO,oBAAI,IAAI,CAAC,aAAa,SAAS,gBAAgB,kBAAkB,CAAC,CAAC;GAC5E;;;ACjPM,MAAM,kBAAkB;;IAE3B,mBAAmB;;IAEnB,qBAAqB;;IAErB,aAAa;;IAEb,cAAc;;AAQX,MAAM,kBAAkB;;IAE3B,WAAW;;IAEX,qBAAqB;;IAErB,aAAa;;IAEb,cAAc;;IAEd,cAAc;;IAEd,WAAW;;;;ACtBf,MAAM,OAAO;IACX,kBAAkB,OAAO,gBAAgB,iBAAiB;IAC1D,mBAAmB,OAAO,gBAAgB,mBAAmB;IAC7D,YAAY,OAAO,gBAAgB,WAAW;IAC9C,aAAa,OAAO,gBAAgB,YAAY;;AAGlD,MAAM,OAAO;IACX,WAAW,OAAO,gBAAgB,SAAS;IAC3C,mBAAmB,OAAO,gBAAgB,mBAAmB;IAC7D,YAAY,OAAO,gBAAgB,WAAW;IAC9C,aAAa,OAAO,gBAAgB,YAAY;IAChD,aAAa,OAAO,gBAAgB,YAAY;IAChD,WAAW,OAAO,gBAAgB,SAAS;;;;ACoH7C,WAAS,gBAAgB,SAAmB;AACxC,UAAM,SAAS,QAAQ;AACvB,QAAI,WAAW;AAAW,aAAO;AACjC,UAAM,QAAQ,QAAQ;AACtB,QAAI,UAAU,WAAc,SAAS,MAAM,QAAQ,yBAAyB;AAAO,aAAO;AAE1F,QAAI,QAAQ,cAAc,QAAW;AACjC,UAAI,UAAU,UAAa,QAAQ,yBAAyB;AAAO,eAAO;AAC1E,YAAM,iBAAiB,SAAS;AAEhC,YAAM,SAAS,QAAQ,YAAY;AACnC,YAAM,YAAY,SAAS;AAC3B,YAAM,WAAW,UAAU,UAAU,KAAK,iBAAiB,KAAK,EAAE,iBAAiB,QAAQ;AAC3F,UAAI,WAAW,aAAa,WAAW;AAAS,eAAO;AACvD,aAAO,OAAO,OAAO,EAAE,OAAO,QAAQ,WAAW,gBAAgB,QAAQ,UAAU,SAAY,SAAS,QAAO,CAAE;IACrH;AAEA,WAAO,OAAO,OAAO;MACjB,OAAO;MACP,gBAAgB;MAChB,QAAQ,QAAQ,yBAAyB,OAAO,SAC1C,QAAQ,yBAAyB,SAAS,UAAU,SAAY,UAAU;KACnF;EACL;AAeM,WAAU,iBACZ,SACA,SAAmB;AAEnB,UAAM,QAAQ,QAAQ,mBAAmB,gBAAgB;AACzD,UAAM,YAAY,QAAQ,qBAAqB,SACzC,OAAO,QAAQ,gBAAgB,IAC/B;AACN,UAAM,SAAS,gBAAgB,OAAO;AAEtC,WAAO;MACH,MAAM,QAAQ,QAAQ;MACtB;MACA,GAAI,WAAW,SAAY,EAAE,iBAAiB,OAAM,IAAK,CAAA;MACzD,GAAI,QAAQ,aAAa,SAAY,EAAE,UAAU,QAAQ,SAAQ,IAAK,CAAA;MACtE,MAAM;;EAEd;AAaM,WAAU,iBACZ,SACA,SAAmB;AAEnB,UAAM,YAAY,QAAQ,qBAAqB,SACzC,OAAO,QAAQ,gBAAgB,IAC/B;AACN,UAAM,SAAS,gBAAgB,OAAO;AAEtC,WAAO;MACH,MAAM;MACN;MACA,GAAI,WAAW,SAAY,EAAE,iBAAiB,OAAM,IAAK,CAAA;MACzD,GAAI,QAAQ,aAAa,SAAY,EAAE,UAAU,QAAQ,SAAQ,IAAK,CAAA;MACtE,MAAM;;EAEd;;;AC5LO,MAAM,mBAAN,MAAuB;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IAGA,YAAY,oBAAI,IAAwB;AAAA;AAAA,IAGxC,sBAAsB,oBAAI,IAAoB;AAAA;AAAA,IAGvD,uBAAuB;AAAA;AAAA,IAGvB,sBAAsB,CAAC;AAAA;AAAA,IAGd,cAAc,oBAAI,IAAY;AAAA;AAAA,IAGvC,yBAAwC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOxC,mBAAmB,CAAC;AAAA;AAAA,IAGpB,6BAA6B,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAU9B,eAAe;AAAA;AAAA,IAGf,cAAc;AAAA;AAAA,IAGd,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOnB,aAAa;AAAA;AAAA,IAGJ;AAAA;AAAA,IAGA;AAAA;AAAA,IAEA;AAAA;AAAA,IAGT;AAAA,IACA;AAAA,IAER,YAAY,MAAuB;AAC/B,WAAK,YAAY,KAAK;AACtB,WAAK,QAAQ,KAAK;AAClB,WAAK,OAAO,KAAK;AACjB,WAAK,YAAY,KAAK;AACtB,WAAK,UAAU,KAAK;AACpB,WAAK,WAAW,KAAK;AACrB,WAAK,aAAa,KAAK,aAAa;AAKpC,YAAM,YAAY,KAAK,cAAc,YAAY,KAAK,UAAU;AAChE,WAAK,oBAAoB,YAClB,KAAK,OAAO,qBAAqB,IAClC;AACN,WAAK,mBAAmB,YACjB,KAAK,OAAO,oBAAoB,IACjC;AAEN,WAAK,SAAS,IAAI,aAAa,KAAK,OAAO,cAAc;AACzD,WAAK,cAAc,IAAI,YAAY;AAAA,QAC/B,cAAc,KAAK,OAAO;AAAA,QAC1B,OAAO,KAAK;AAAA,MAChB,CAAC;AACD,WAAK,eAAe,IAAI,oBAAoB,KAAK,SAAS;AAG1D,WAAK,oBAAoB,KAAK,OAAO,oBAC/B,IAAI,4BAA4B,wBAAwB,IACxD;AAAA,IAGV;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,SAAS,OAAqB;AAC1B,WAAK,cAAc;AAAA,IACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,IAAI,wBAAgC;AAChC,UAAI,KAAK,mBAAmB;AACxB,eAAO,KAAK,kBAAkB,wBAAwB;AAAA,MAC1D;AACA,aAAO,KAAK,YAAY;AAAA,IAC5B;AAAA;AAAA,IAGA,IAAI,iBAAyB;AACzB,aAAO,KAAK;AAAA,IAChB;AAAA;AAAA,IAGA,IAAI,qBAA6B;AAC7B,aAAO,KAAK,oBAAoB;AAAA,IACpC;AAAA;AAAA,IAGA,IAAI,YAAqB;AACrB,aAAO,KAAK;AAAA,IAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,eAAe,IAA2B;AACtC,cAAQ,GAAG,MAAM;AAAA,QACb,KAAK;AACD,eAAK,aAAa,GAAG,SAAS,GAAG;AACjC;AAAA,QAEJ,KAAK;AAKD,eAAK,aAAa;AAQlB;AACI,kBAAM,WAAW,KAAK,aAAa,UAAU;AAC7C,iBAAK,gBAAgB,QAAQ;AAAA,UACjC;AAEA,cAAI,KAAK,UAAU;AACf,kBAAM,SAAS,KAAK,SAAS,SAAS,EAAE,MAAM,gBAAgB,SAAS,GAAG,QAAQ,CAAC;AACnF,iBAAK,QAAQ,EAAE,MAAM,YAAY,OAAO,CAAC;AAAA,UAC7C;AACA;AAAA,QAEJ,KAAK;AAWD,cAAI,GAAG,sBAAsB,QAAW;AACpC,iBAAK,KAAK,yBAAyB,GAAG,mBAAmB,GAAG,cAAc;AAC1E,kBAAM,QAAQ,KAAK,KAAK;AACxB,gBAAI,KAAK,KAAK,2BAA2B;AACrC,mBAAK,QAAQ,EAAE,MAAM,cAAc,SAAS,MAAM,CAAC;AAAA,YACvD;AAAA,UACJ;AACA;AAAA,QAEJ,KAAK;AAED;AAAA,MACR;AAAA,IACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAWA,UAAU,QAA0B;AAChC,YAAM,WAAW,KAAK,aAAa,UAAU,MAAM;AACnD,UAAI,SAAS,WAAW,aAAa;AACjC,aAAK,UAAU;AAAA,UACX,MAAM;AAAA,UACN,WAAW,KAAK;AAAA,UAChB;AAAA,QACJ,CAAC;AAAA,MACL;AAAA,IACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUA,WAAW,KAAiB,SAA4B;AACpD,UAAI,IAAI,SAAS,OAAO;AAEpB,YAAI,IAAI,WAAW,aAAa,cAAc;AAC1C,eAAK,YAAY,kBAAkB,IAAI,OAAO;AAC9C,eAAK,YAAY,IAAI,IAAI,OAAiB;AAAA,QAC9C,WAAW,IAAI,WAAW,aAAa,cAAc;AACjD,eAAK,YAAY,kBAAkB;AAAA,QACvC;AACA;AAAA,MACJ;AAMA,UAAI,IAAI,UAAU,KAAK,iBAAkB;AACzC,UAAI,KAAK,uBAAuB,MAAM,IAAI,UAAU,KAAK,oBAAqB;AAE9E,UAAI,IAAI,YAAY,KAAK,uBAAuB,IAAI,WAAW,KAAK,qBAAsB;AAG1F,YAAM,MAAM,GAAG,IAAI,OAAO,IAAI,IAAI,QAAQ;AAC1C,UAAI,KAAK,UAAU,IAAI,GAAG,EAAG;AAG7B,UAAI,KAAK,OAAO,OAAO,GAAG,GAAG;AACzB,aAAK,UAAU,IAAI,KAAK,WAAW,CAAC,CAAC;AACrC,cAAM,QAAQ,KAAK,oBAAoB,IAAI,IAAI,OAAO,KAAK;AAC3D,aAAK,oBAAoB,IAAI,IAAI,SAAS,QAAQ,CAAC;AAAA,MACvD,WAAW,IAAI,sBAAsB,QAAW;AAE5C,YAAI,CAAC,KAAK,kBAAkB,KAAK,OAAO,KAAK,KAAK,UAAU;AACxD,eAAK,QAAQ;AAAA,YACT,MAAM;AAAA,YACN,QAAQ,KAAK,SAAS,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAAA,UAC9D,CAAC;AAAA,QACL;AAAA,MACJ,WAAW,KAAK,UAAU;AAEtB,aAAK,QAAQ;AAAA,UACT,MAAM;AAAA,UACN,QAAQ,KAAK,SAAS,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAAA,QAC9D,CAAC;AAAA,MACL;AAIA,UAAI,KAAK,qBAAqB,SAAS,qBAAqB,QAAW;AACnE,cAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,cAAM,YAAY,OAAO,QAAQ,gBAAgB;AACjD,aAAK,kBAAkB,eAAe,OAAO,WAAW,KAAK;AAAA,MACjE;AAGA,WAAK,YAAY,aAAa,IAAI,OAAO;AAAA,IAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IA+BA,oBAAoB,cAA4B;AAK5C,WAAK,OAAO,MAAM;AAClB,WAAK,UAAU,MAAM;AACrB,WAAK,oBAAoB,MAAM;AAC/B,WAAK,sBAAsB,eAAe;AAC1C,WAAK,YAAY,IAAI,KAAK,mBAAmB;AAC7C,WAAK,uBAAuB;AAC5B,WAAK,mBAAmB;AACxB,WAAK,6BAA6B,CAAC;AACnC,WAAK,aAAa;AAClB,WAAK,yBAAyB;AAC9B,WAAK,YAAY,MAAM;AAGvB,YAAM,WAAW,KAAK,aAAa,UAAU;AAC7C,WAAK,gBAAgB,QAAQ;AAAA,IACjC;AAAA,IAEA,MAAM,eAA8B;AAChC,WAAK,OAAO,MAAM;AAClB,WAAK,UAAU,MAAM;AACrB,WAAK,oBAAoB,MAAM;AAC/B,WAAK,sBAAsB,CAAC;AAC5B,WAAK,uBAAuB;AAC5B,WAAK,mBAAmB,iBAAiB,CAAC;AAC1C,WAAK,6BAA6B,CAAC;AACnC,WAAK,eAAe;AACpB,WAAK,aAAa;AAClB,WAAK,cAAc;AACnB,WAAK,YAAY,MAAM;AACvB,WAAK,yBAAyB;AAC9B,WAAK,YAAY,MAAM;AACvB,WAAK,mBAAmB,MAAM;AAC9B,YAAM,WAAW,KAAK,aAAa,UAAU;AAC7C,WAAK,gBAAgB,QAAQ;AAAA,IACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,OAAa;AAET,UAAI,KAAK,mBAAmB;AACxB,cAAM,QAAQ,KAAK,MAAM,IAAI,IAAI;AACjC,aAAK,kBAAkB,KAAK,KAAK;AACjC,aAAK,YAAY,eAAe,KAAK,kBAAkB,wBAAwB;AAC/E,aAAK,KAAK,mBAAmB,KAAK,kBAAkB,4BAA4B;AAAA,MACpF;AAOA,UAAI,KAAK,gBAAgB,KAAK,OAAO,OAAO,GAAG;AAC3C,cAAM,MAAM,KAAK,OAAO,KAAK;AAI7B,YAAI,IAAI,aAAa,IAAI;AACrB,eAAK,OAAO,QAAQ;AACpB,eAAK,oBAAoB,IAAI,OAAO;AACpC;AAAA,QACJ;AACA,aAAK,sBAAsB,IAAI,UAAU;AACzC,aAAK,YAAY,IAAI,KAAK,mBAAmB;AAC7C,aAAK,eAAe;AAAA,MACxB;AAGA,YAAM,oBAAoB,CAAC,GAAG,KAAK,oBAAoB,KAAK,CAAC;AAC7D,YAAM,cAAc,KAAK,YAAY,SAAS,KAAK,qBAAqB,iBAAiB;AAGzF,UAAI,YAAY,WAAW,UAAU,aAAa;AAC9C,YAAI,CAAC,KAAK,aAAa;AACnB,eAAK,cAAc;AACnB,gBAAM,WAAW,KAAK,aAAa,iBAAiB;AACpD,eAAK,gBAAgB,QAAQ;AAC7B,eAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAAA,QACxC;AACA;AAAA,MACJ;AAEA,UAAI,YAAY,WAAW,UAAU,gBAAgB,YAAY,kBAAkB,QAAW;AAC1F,cAAM,SAAS,YAAY;AAI3B,YAAI,KAAK,cAAc,WAChB,KAAK,uBAAuB,MAC5B,CAAC,KAAK,YAAY,IAAI,KAAK,mBAAmB,GAAG;AACpD,eAAK,QAAQ;AAAA,YACT,MAAM;AAAA,YACN,aAAa,KAAK;AAAA,YAClB,WAAW;AAAA,YACX,QAAQ;AAAA,UACZ,CAAkB;AAAA,QACtB;AAEA,mBAAW,OAAO,KAAK,aAAa;AAChC,cAAI,MAAM,OAAQ,MAAK,YAAY,OAAO,GAAG;AAAA,QACjD;AAEA,aAAK,QAAQ;AAAA,UACT,MAAM;AAAA,UACN,aAAa,KAAK;AAAA,UAClB,WAAW;AAAA,QACf,CAAC;AAGD,YAAI,KAAK,UAAU;AACf,gBAAM,SAAS,KAAK,SAAS,SAAS,EAAE,MAAM,OAAO,SAAS,OAAO,CAAC;AACtE,eAAK,QAAQ,EAAE,MAAM,YAAY,OAAO,CAAC;AAAA,QAC7C;AAGA,aAAK,mBAAmB;AAGxB,aAAK,cAAc,MAAM;AASzB,cAAM,WAAW,KAAK,aAAa,UAAU;AAC7C,aAAK,gBAAgB,QAAQ;AAG7B,aAAK,sBAAsB,SAAS;AACpC,aAAK,YAAY,IAAI,KAAK,mBAAmB;AAC7C,aAAK,uBAAuB;AAC5B,aAAK,yBAAyB;AAAA,MAClC;AAOA,UAAI,KAAK,mBAAmB,KAAK,KAAK,oBAAoB,OAAO,KAAK,kBAAkB;AACpF,aAAK,YAAY;AAAA,MACrB;AAQA,UAAI,KAAK,WAAY;AAOrB,UAAI,KAAK,cAAc,WAAW,KAAK,2BAA2B,MAAM;AACpE,cAAM,UAAU,KAAK,MAAM,IAAI,IAAI,KAAK;AACxC,YAAI,UAAU,KAAK,YAAY,cAAc;AACzC,gBAAM,cAAc,KAAK,sBAAsB;AAC/C,eAAK,QAAQ;AAAA,YACT,MAAM;AAAA,YACN,aAAa,KAAK;AAAA,YAClB,WAAW;AAAA,YACX,QAAQ;AAAA,UACZ,CAAkB;AAClB,eAAK,YAAY,IAAI,KAAK,mBAAmB;AAC7C,gBAAM,WAAW,KAAK,aAAa,UAAU;AAC7C,eAAK,gBAAgB,QAAQ;AAC7B,eAAK,yBAAyB;AAAA,QAClC;AAAA,MACJ;AAQA,UAAI,WAAW;AACf,YAAM,SAAS,KAAK;AAEpB,aAAO,KAAK,OAAO,OAAO,GAAG;AAEzB,YAAI,SAAS,KAAK,YAAY,OAAQ;AAGtC,YAAI,KAAK,WAAY;AAErB,cAAM,MAAM,KAAK,OAAO,KAAK;AAC7B,YAAI,CAAC,IAAK;AAEV,cAAM,oBAAoB,KAAK,sBAAsB;AAGrD,YAAI,IAAI,UAAU,mBAAmB;AACjC;AAAA,QACJ;AAMA,YAAI,IAAI,UAAU,KAAK,uBAAuB,KAAK,uBAAuB,IAAI;AAC1E,cAAI,KAAK,cAAc,WAAW,CAAC,KAAK,YAAY,IAAI,KAAK,mBAAmB,GAAG;AAC/E,gBAAI,KAAK,2BAA2B,MAAM;AACtC,mBAAK,yBAAyB,KAAK,MAAM,IAAI;AAAA,YACjD;AACA;AAAA,UACJ;AACA,eAAK,yBAAyB;AAC9B,eAAK,YAAY,OAAO,KAAK,mBAAmB;AAAA,QACpD;AAGA,YAAI,IAAI,UAAU,KAAK,qBAAqB;AACxC,eAAK,uBAAuB;AAAA,QAChC;AAMA,YAAI,KAAK,cAAc,WAAW,IAAI,WAAW,KAAK,sBAAsB;AAGxE,gBAAM,cAAc,IAAI;AACxB,iBAAO,KAAK,OAAO,OAAO,GAAG;AACzB,kBAAM,OAAO,KAAK,OAAO,KAAK;AAC9B,gBAAI,CAAC,QAAQ,KAAK,YAAY,YAAa;AAC3C,iBAAK,OAAO,QAAQ;AACpB,iBAAK,oBAAoB,WAAW;AAAA,UACxC;AACA,eAAK,sBAAsB;AAC3B,eAAK,uBAAuB;AAE5B,gBAAM,WAAW,KAAK,aAAa,UAAU;AAC7C,eAAK,gBAAgB,QAAQ;AAC7B;AAAA,QACJ;AAEA,cAAM,MAAM,KAAK,OAAO,QAAQ;AAChC,aAAK,oBAAoB,IAAI,OAAO;AAEpC,YAAI,IAAI,SAAS,QAAQ;AACrB,eAAK,kBAAkB,GAAG;AAAA,QAC9B;AAEA,aAAK,uBAAuB,IAAI,WAAW;AAC3C;AAGA,YAAI,IAAI,UAAU,KAAK,qBAAqB;AACxC,eAAK,sBAAsB,IAAI;AAAA,QACnC;AAAA,MACJ;AAAA,IACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASQ,cAAoB;AACxB,YAAM,WAAW,CAAC,GAAG,KAAK,oBAAoB,KAAK,CAAC,EAAE;AAAA,QAAK,CAAC,GAAG,MAC3D,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI;AAAA,MAC7B;AACA,YAAM,SAAS,SAAS,SAAS,KAAK;AACtC,UAAI,UAAU,EAAG;AAEjB,YAAM,eAAe,SAAS,MAAM,GAAG,MAAM;AAC7C,YAAM,SAAS,aAAa,aAAa,SAAS,CAAC,IAAK;AACxD,WAAK,OAAO,cAAc,MAAM;AAChC,iBAAW,OAAO,cAAc;AAC5B,aAAK,oBAAoB,OAAO,GAAG;AAAA,MACvC;AAEA,iBAAW,OAAO,KAAK,UAAU,KAAK,GAAG;AACrC,cAAM,UAAU,OAAO,IAAI,MAAM,GAAG,EAAE,CAAC,CAAE;AACzC,YAAI,UAAU,OAAQ,MAAK,UAAU,OAAO,GAAG;AAAA,MACnD;AAGA,YAAM,WAAW,aAAa,aAAa,SAAS,CAAC;AACrD,UAAI,YAAY,KAAK,qBAAqB;AACtC,aAAK,sBAAsB;AAAA,MAC/B;AACA,WAAK,mBAAmB,WAAW;AAGnC,YAAM,WAAW,KAAK,aAAa,UAAU;AAC7C,WAAK,gBAAgB,QAAQ;AAE7B,WAAK,QAAQ;AAAA,QACT,MAAM;AAAA,QACN,eAAe;AAAA,QACf,iBAAiB,KAAK,oBAAoB;AAAA,QAC1C,QAAQ,oBAAoB,KAAK,gBAAgB;AAAA,MACrD,CAAC;AAAA,IACL;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAkBQ,qBAAqB,SAAiD;AAC1E,UAAI,YAAY,OAAW,QAAO;AAClC,UAAI,QAAQ,YAAa,QAAO;AAChC,aAAO,QAAQ,gBAAgB;AAAA,IACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOQ,kBAAkB,KAAqB,SAA+B;AAC1E,YAAM,UAAU,KAAK,OAAO,sBAAsB;AAClD,UAAI,CAAC,QAAS,QAAO;AAErB,UAAI,QAAQ,oBAAqB,IAAI,mBAAoB;AAErD,aAAK,oBAAoB,QAAQ,OAAO;AACxC,cAAM,aAAa,GAAG,QAAQ,OAAO,IAAI,QAAQ,QAAQ;AACzD,aAAK,UAAU,OAAO,UAAU;AAEhC,aAAK,OAAO,OAAO,GAAG;AACtB,cAAM,MAAM,GAAG,IAAI,OAAO,IAAI,IAAI,QAAQ;AAC1C,aAAK,UAAU,IAAI,KAAK,WAAW,CAAC,CAAC;AACrC,cAAM,QAAQ,KAAK,oBAAoB,IAAI,IAAI,OAAO,KAAK;AAC3D,aAAK,oBAAoB,IAAI,IAAI,SAAS,QAAQ,CAAC;AACnD,eAAO;AAAA,MACX,OAAO;AAEH,aAAK,OAAO,OAAO,OAAO;AAC1B,eAAO;AAAA,MACX;AAAA,IACJ;AAAA,IAEQ,kBAAkB,KAA2B;AACjD,YAAM,MAAM,GAAG,IAAI,OAAO,IAAI,IAAI,QAAQ;AAC1C,YAAM,UAAU,KAAK,UAAU,IAAI,GAAG,KAAK,CAAC;AAC5C,WAAK,UAAU,OAAO,GAAG;AAKzB,YAAM,SAAS,KAAK,cAAc,UAAU,QAAQ,cAAc,QAAQ;AAC1E,UAAI,QAAQ;AACR,cAAM,iBAAiB,KAAK,aAAa,UAAU,MAAM;AACzD,YAAI,eAAe,WAAW,aAAa;AACvC,eAAK,UAAU;AAAA,YACX,MAAM;AAAA,YACN,WAAW,KAAK;AAAA,YAChB;AAAA,UACJ,CAAC;AAAA,QACL;AAAA,MACJ;AAKA,UAAI,QAAQ,qBAAqB,UAAa,CAAC,KAAK,KAAK,cAAc;AACnE,YAAI,KAAK,cAAc,SAAS;AAC5B,eAAK,KAAK,kBAAkB,QAAQ,gBAAgB;AAAA,QACxD,WAAW,KAAK,YAAY;AACxB,eAAK,KAAK,kBAAkB,QAAQ,gBAAgB;AAAA,QACxD;AAAA,MACJ;AAMA,YAAM,eAAe,KAAK,cAAc,UACjC,QAAQ,sBACH,IAAI,aAAa,KACf;AAAA,QAAE,aAAa;AAAA,QAAM,cAAc;AAAA,QAAM,YAAY;AAAA,QACnD,aAAa;AAAA,QAAO,eAAe;AAAA,QAAO,YAAY;AAAA,MAAE,IAC1D,UACR;AAEN,UAAI;AACJ,UAAI,QAAQ,qBAAqB,QAAW;AACxC,cAAM,SAAS,KAAK,cAAc,UAC5B,KAAK,KAAK,uBAAuB,QAAQ,gBAAgB,IACzD,KAAK,KAAK,uBAAuB,QAAQ,gBAAgB;AAE/D,YAAI,WAAW,MAAM;AAGjB,yBAAe;AAAA,QACnB,WAAW,OAAO,YAAY;AAC1B,cAAI,OAAO,WAAW,MAAY;AAC9B,2BAAe,KAAK,MAAM,IAAI;AAAA,UAClC,WAAW,KAAK,cAAc,WAAW,CAAC,KAAK,qBAAqB,YAAY,GAAG;AAQ/E,2BAAe,KAAK,MAAM,IAAI;AAAA,UAClC,OAAO;AACH;AAAA,UACJ;AAAA,QACJ,WAAW,OAAO,WAAW,KAAW;AACpC,yBAAe,KAAK,MAAM,IAAI;AAAA,QAClC,OAAO;AACH,yBAAe,OAAO;AAAA,QAC1B;AAAA,MACJ,OAAO;AACH,uBAAe,KAAK,MAAM,IAAI;AAAA,MAClC;AAKA,UAAI,QAAQ,qBAAqB,QAAW;AACxC,cAAM,UAAU,KAAK,KAAK,gBAAgB,QAAQ,kBAAkB,QAAQ,oBAAoB;AAChG,YAAI,YAAY,QAAQ,QAAQ,gBAAgB,KAAK,kBAAkB;AACnE,eAAK,mBAAmB,QAAQ;AAChC,eAAK,QAAQ,EAAE,MAAM,oBAAoB,OAAO,QAAQ,CAAC;AACzD,eAAK,UAAU,EAAE,MAAM,qBAAqB,MAAM,QAAQ,YAAY,CAAC;AAAA,QAC3E;AAAA,MACJ;AAGA,UAAI,KAAK,cAAc,SAAS;AAG5B,cAAM,UAAU;AAKhB,YAAI,SAAS,eAAe,KAAK,eAAe,IAAI,QAAQ,SAAS,GAAG;AACpE,gBAAM,aAAa,kBAAkB,KAAK,aAAa,IAAI,OAAO;AAClE,cAAI,eAAe,OAAO;AACtB,iBAAK,QAAQ;AAAA,cACT,MAAM;AAAA,cACN,SAAS,IAAI;AAAA,cACb,UAAU,IAAI;AAAA,cACd,OAAO,KAAK;AAAA,YAChB,CAAC;AAAA,UACL;AAAA,QACJ;AAEA,cAAM,qBAAqB,YAAY,SACjC,EAAE,GAAG,SAAS,mBAAmB,QAAQ,IACzC;AACN,cAAM,QAAQ,iBAAiB,IAAI,SAAS,kBAAkB;AAC9D,cAAM,YAAY,KAAK,aAAa;AACpC,cAAM,WAAW,KAAK,aAAa,kBAAkB,OAAO,OAAO;AAInE,YAAI,cAAc,oBAAoB,KAAK,aAAa,UAAU,cAC3D,QAAQ,qBAAqB,QAAW;AAC3C,eAAK,KAAK,YAAY,QAAQ,gBAAgB;AAE9C,gBAAM,aAAa,KAAK,KAAK,uBAAuB,QAAQ,gBAAgB;AAC5E,cAAI,YAAY;AACZ,2BAAe,WAAW;AAAA,UAC9B;AAAA,QACJ;AAEA,YAAI,SAAS,WAAW,UAAU;AAC9B,eAAK,UAAU;AAAA,YACX,MAAM;AAAA,YACN,OAAO,SAAS;AAAA,YAChB;AAAA,YACA,GAAI,QAAQ,qBAAqB,SAC3B,EAAE,oBAAoB,QAAQ,iBAAiB,IAC/C,CAAC;AAAA,UACX,CAAC;AAAA,QACL,WAAW,SAAS,WAAW,QAAQ;AAEnC,cAAI,SAAS,WAAW,0BAA0B,IAAI,YAAY,KAAK,4BAA4B;AAC/F,iBAAK,6BAA6B,IAAI;AACtC,iBAAK,QAAQ,EAAE,MAAM,oBAAoB,SAAS,IAAI,QAAQ,CAAC;AAAA,UACnE;AAAA,QACJ;AAAA,MACJ,OAAO;AACH,cAAM,QAAQ,iBAAiB,IAAI,SAAS,OAAO;AACnD,cAAM,WAAW,KAAK,aAAa,kBAAkB,KAAK;AAE1D,YAAI,SAAS,WAAW,UAAU;AAC9B,eAAK,UAAU;AAAA,YACX,MAAM;AAAA,YACN,OAAO,SAAS;AAAA,YAChB;AAAA,UACJ,CAAC;AAAA,QACL;AAAA,MACJ;AAAA,IACJ;AAAA,IAEQ,gBAAgB,UAAqD;AACzE,UAAI,SAAS,WAAW,SAAS;AAC7B,aAAK,UAAU;AAAA,UACX,MAAM;AAAA,UACN,WAAW,KAAK;AAAA,UAChB,QAAS,SAAiB,UAAU;AAAA,QACxC,CAAC;AAAA,MACL,WAAW,SAAS,WAAW,SAAS;AACpC,aAAK,UAAU;AAAA,UACX,MAAM;AAAA,UACN,WAAW,KAAK;AAAA,QACpB,CAAC;AAAA,MACL;AAAA,IACJ;AAAA,IAEQ,cAAc,SAAuB;AAEzC,WAAK,OAAO,cAAc,OAAO;AAGjC,iBAAW,CAAC,GAAG,KAAK,KAAK,qBAAqB;AAC1C,YAAI,MAAM,SAAS;AACf,eAAK,oBAAoB,OAAO,GAAG;AAAA,QACvC;AAAA,MACJ;AAGA,iBAAW,CAAC,GAAG,KAAK,KAAK,WAAW;AAChC,cAAM,WAAW,IAAI,QAAQ,GAAG;AAChC,cAAM,MAAM,OAAO,IAAI,MAAM,GAAG,QAAQ,CAAC;AACzC,YAAI,MAAM,SAAS;AACf,eAAK,UAAU,OAAO,GAAG;AAAA,QAC7B;AAAA,MACJ;AAAA,IACJ;AAAA,IAEQ,oBAAoB,SAAuB;AAC/C,YAAM,QAAQ,KAAK,oBAAoB,IAAI,OAAO;AAClD,UAAI,UAAU,QAAW;AACrB,YAAI,SAAS,GAAG;AACZ,eAAK,oBAAoB,OAAO,OAAO;AAAA,QAC3C,OAAO;AACH,eAAK,oBAAoB,IAAI,SAAS,QAAQ,CAAC;AAAA,QACnD;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;;;AC95BO,MAAM,qBAAN,MAAyB;AAAA,IACX;AAAA,IACA;AAAA,IACT,cAA6B;AAAA,IAC7B,kBAAiC;AAAA,IACjC,iBAAgC;AAAA,IAChC,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAStB,YAAY,QAAQ,KAAK,aAAa,GAAG;AACrC,WAAK,QAAQ;AACb,WAAK,aAAa;AAAA,IACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA,YAAY,OAAe,aAA2B;AAClD,UAAI,KAAK,oBAAoB,QAAQ,KAAK,mBAAmB,MAAM;AAC/D,cAAM,UAAU,cAAc,KAAK;AACnC,YAAI,UAAU,GAAG;AACb,eAAK;AACL,gBAAM,aAAc,QAAQ,KAAM,UAAU;AAC5C,cAAI,KAAK,gBAAgB,MAAM;AAC3B,iBAAK,cAAc;AAAA,UACvB,OAAO;AACH,iBAAK,cAAc,KAAK,QAAQ,cAAc,IAAI,KAAK,SAAS,KAAK;AAAA,UACzE;AAAA,QACJ;AAAA,MACJ;AACA,WAAK,kBAAkB;AACvB,WAAK,iBAAiB;AAAA,IAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,kBAA0B;AACtB,UAAI,KAAK,gBAAgB,QAAQ,KAAK,cAAc,KAAK,WAAY,QAAO;AAC5E,aAAO,KAAK,cAAc;AAAA,IAC9B;AAAA;AAAA,IAGA,QAAc;AACV,WAAK,cAAc;AACnB,WAAK,kBAAkB;AACvB,WAAK,iBAAiB;AACtB,WAAK,cAAc;AAAA,IACvB;AAAA,EACJ;;;ACnBO,MAAM,wBAAN,MAA4B;AAAA,IACd;AAAA,IACR;AAAA;AAAA,IAEA;AAAA,IACQ;AAAA,IACT;AAAA,IAER,YAAY,QAA2B;AACnC,WAAK,SAAS,OAAO;AACrB,WAAK,iBAAiB,OAAO;AAC7B,WAAK,kBAAkB,OAAO;AAC9B,WAAK,kBAAkB,OAAO,4BAA4B;AAC1D,WAAK,eAAe,OAAO;AAAA,IAC/B;AAAA;AAAA,IAGA,IAAI,QAAgB;AAAE,aAAO,KAAK;AAAA,IAAc;AAAA;AAAA,IAGhD,IAAI,eAAqC;AAAE,aAAO,KAAK,OAAO,KAAK,YAAY;AAAA,IAAG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAelF,SAAS,SAAkC;AAEvC,UAAI,QAAQ,gBAAgB,KAAK,gBAAgB;AAC7C,YAAI,KAAK,eAAe,KAAK,OAAO,SAAS,GAAG;AAC5C,iBAAO,EAAE,QAAQ,aAAa,aAAa,KAAK,eAAe,EAAE;AAAA,QACrE;AACA,eAAO,EAAE,QAAQ,OAAO;AAAA,MAC5B;AAKA,UAAI,KAAK,eAAe,KAAK,QAAQ,iBAAiB,KAAK,iBAAiB;AACxE,cAAM,cAAc,KAAK,OAAO,KAAK,eAAe,CAAC;AACrD,YAAI,QAAQ,wBAAwB,YAAY,cAAc,KAAK,iBAAiB;AAChF,iBAAO,EAAE,QAAQ,WAAW,aAAa,KAAK,eAAe,EAAE;AAAA,QACnE;AAAA,MACJ;AAEA,aAAO,EAAE,QAAQ,OAAO;AAAA,IAC5B;AAAA;AAAA,IAGA,kBAAwB;AACpB,UAAI,KAAK,eAAe,KAAK,OAAO,SAAS,EAAG,MAAK;AAAA,IACzD;AAAA;AAAA,IAGA,gBAAsB;AAClB,UAAI,KAAK,eAAe,EAAG,MAAK;AAAA,IACpC;AAAA;AAAA,IAGA,SAAS,OAAqB;AAC1B,WAAK,eAAe,KAAK,IAAI,GAAG,KAAK,IAAI,OAAO,KAAK,OAAO,SAAS,CAAC,CAAC;AAAA,IAC3E;AAAA,EACJ;","names":[]}