| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Common from '../../core/common/common.js'; |
| import * as SDK from '../../core/sdk/sdk.js'; |
| import type * as Protocol from '../../generated/protocol.js'; |
| |
| interface EventWithTimestamp { |
| event: Protocol.Network.DeviceBoundSessionEventOccurredEvent; |
| timestamp: Date; |
| } |
| export interface SessionAndEvents { |
| session?: Protocol.Network.DeviceBoundSession; |
| isSessionTerminated: boolean; |
| hasErrors: boolean; |
| eventsById: Map<string, EventWithTimestamp>; |
| } |
| type SessionIdToSessionMap = Map<string|undefined, SessionAndEvents>; |
| |
| export class DeviceBoundSessionsModel extends Common.ObjectWrapper.ObjectWrapper<DeviceBoundSessionModelEventTypes> |
| implements SDK.TargetManager.SDKModelObserver<SDK.NetworkManager.NetworkManager> { |
| #siteSessions = new Map<string, SessionIdToSessionMap>(); |
| #visibleSites = new Set<string>(); |
| |
| constructor() { |
| super(); |
| SDK.TargetManager.TargetManager.instance().observeModels(SDK.NetworkManager.NetworkManager, this, {scoped: true}); |
| } |
| |
| modelAdded(networkManager: SDK.NetworkManager.NetworkManager): void { |
| networkManager.addEventListener(SDK.NetworkManager.Events.DeviceBoundSessionsAdded, this.#onSessionsSet, this); |
| networkManager.addEventListener( |
| SDK.NetworkManager.Events.DeviceBoundSessionEventOccurred, this.#onEventOccurred, this); |
| void networkManager.enableDeviceBoundSessions(); |
| } |
| |
| modelRemoved(networkManager: SDK.NetworkManager.NetworkManager): void { |
| networkManager.removeEventListener(SDK.NetworkManager.Events.DeviceBoundSessionsAdded, this.#onSessionsSet, this); |
| networkManager.removeEventListener( |
| SDK.NetworkManager.Events.DeviceBoundSessionEventOccurred, this.#onEventOccurred, this); |
| } |
| |
| addVisibleSite(site: string): void { |
| if (this.#visibleSites.has(site)) { |
| return; |
| } |
| this.#visibleSites.add(site); |
| this.dispatchEventToListeners(DeviceBoundSessionModelEvents.ADD_VISIBLE_SITE, {site}); |
| } |
| |
| clearVisibleSites(): void { |
| if (this.getPreserveLogSetting().get()) { |
| return; |
| } |
| this.#visibleSites.clear(); |
| this.dispatchEventToListeners(DeviceBoundSessionModelEvents.CLEAR_VISIBLE_SITES); |
| } |
| |
| clearEvents(): void { |
| if (this.getPreserveLogSetting().get()) { |
| return; |
| } |
| const emptySessions = new Map<string, Array<string|undefined>>(); |
| const noLongerFailedSessions = new Map<string, Array<string|undefined>>(); |
| const emptySites = new Set<string>(); |
| for (const [site, sessionIdToSessionMap] of [...this.#siteSessions]) { |
| let emptySessionsSiteEntry = emptySessions.get(site); |
| let noLongerFailedSessionsSiteEntry = noLongerFailedSessions.get(site); |
| for (const [sessionId, sessionAndEvents] of sessionIdToSessionMap) { |
| sessionAndEvents.eventsById.clear(); |
| if (sessionAndEvents.hasErrors) { |
| sessionAndEvents.hasErrors = false; |
| if (!noLongerFailedSessionsSiteEntry) { |
| noLongerFailedSessionsSiteEntry = []; |
| noLongerFailedSessions.set(site, noLongerFailedSessionsSiteEntry); |
| } |
| noLongerFailedSessionsSiteEntry.push(sessionId); |
| } |
| if (sessionAndEvents.session) { |
| continue; |
| } |
| // Remove empty sessions. |
| sessionIdToSessionMap.delete(sessionId); |
| if (!emptySessionsSiteEntry) { |
| emptySessionsSiteEntry = []; |
| emptySessions.set(site, emptySessionsSiteEntry); |
| } |
| emptySessionsSiteEntry.push(sessionId); |
| } |
| |
| // Remove empty sites. |
| if (sessionIdToSessionMap.size === 0) { |
| this.#siteSessions.delete(site); |
| emptySites.add(site); |
| } |
| } |
| |
| this.dispatchEventToListeners( |
| DeviceBoundSessionModelEvents.CLEAR_EVENTS, {emptySessions, emptySites, noLongerFailedSessions}); |
| } |
| |
| isSiteVisible(site: string): boolean { |
| return this.#visibleSites.has(site); |
| } |
| |
| isSessionTerminated(site: string, sessionId?: string): boolean { |
| const session = this.getSession(site, sessionId); |
| if (session === undefined) { |
| return false; |
| } |
| return session.isSessionTerminated; |
| } |
| |
| sessionHasErrors(site: string, sessionId?: string): boolean { |
| const session = this.getSession(site, sessionId); |
| if (session === undefined) { |
| return false; |
| } |
| return session.hasErrors; |
| } |
| |
| getSession(site: string, sessionId?: string): SessionAndEvents|undefined { |
| return this.#siteSessions.get(site)?.get(sessionId); |
| } |
| |
| getPreserveLogSetting(): Common.Settings.Setting<boolean> { |
| return Common.Settings.Settings.instance().createSetting('device-bound-sessions-preserve-log', false); |
| } |
| |
| #onSessionsSet({data: sessions}: {data: Protocol.Network.DeviceBoundSession[]}): void { |
| for (const session of sessions) { |
| const sessionAndEvents = this.#ensureSiteAndSessionInitialized(session.key.site, session.key.id); |
| sessionAndEvents.session = session; |
| } |
| this.dispatchEventToListeners(DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS, {sessions}); |
| } |
| |
| #ensureSiteAndSessionInitialized(site: string, sessionId?: string): SessionAndEvents { |
| let sessionIdToSessionMap = this.#siteSessions.get(site); |
| if (!sessionIdToSessionMap) { |
| sessionIdToSessionMap = new Map(); |
| this.#siteSessions.set(site, sessionIdToSessionMap); |
| } |
| |
| let sessionAndEvent = sessionIdToSessionMap.get(sessionId); |
| if (!sessionAndEvent) { |
| sessionAndEvent = { |
| session: undefined, |
| isSessionTerminated: false, |
| hasErrors: false, |
| eventsById: new Map<string, EventWithTimestamp>() |
| }; |
| sessionIdToSessionMap.set(sessionId, sessionAndEvent); |
| } |
| return sessionAndEvent; |
| } |
| |
| #onEventOccurred({data: event}: {data: Protocol.Network.DeviceBoundSessionEventOccurredEvent}): void { |
| const sessionAndEvent = this.#ensureSiteAndSessionInitialized(event.site, event.sessionId); |
| |
| // If this eventId has already been tracked, quit early. |
| if (sessionAndEvent.eventsById.has(event.eventId)) { |
| return; |
| } |
| |
| // Add the new event. |
| const eventWithTimestamp = {event, timestamp: new Date()}; |
| sessionAndEvent.eventsById.set(event.eventId, eventWithTimestamp); |
| |
| // Add the new session if there is one. |
| const newSession = event.creationEventDetails?.newSession || event.refreshEventDetails?.newSession; |
| if (newSession) { |
| sessionAndEvent.session = newSession; |
| } |
| |
| // Add the new challenge onto the session if there is one. |
| if (event.succeeded && sessionAndEvent.session && event.challengeEventDetails) { |
| sessionAndEvent.session.cachedChallenge = event.challengeEventDetails.challenge; |
| } |
| |
| // Set the session's terminated status based on the event. |
| if (event.succeeded) { |
| if (event.terminationEventDetails) { |
| sessionAndEvent.isSessionTerminated = true; |
| } else if (event.creationEventDetails) { |
| sessionAndEvent.isSessionTerminated = false; |
| } |
| } |
| |
| // Set that the session has errors if the latest event failed. |
| if (!event.succeeded) { |
| sessionAndEvent.hasErrors = true; |
| } |
| |
| this.dispatchEventToListeners( |
| DeviceBoundSessionModelEvents.EVENT_OCCURRED, |
| {site: eventWithTimestamp.event.site, sessionId: eventWithTimestamp.event.sessionId}); |
| } |
| } |
| |
| export const enum DeviceBoundSessionModelEvents { |
| INITIALIZE_SESSIONS = 'INITIALIZE_SESSIONS', |
| ADD_VISIBLE_SITE = 'ADD_VISIBLE_SITE', |
| CLEAR_VISIBLE_SITES = 'CLEAR_VISIBLE_SITES', |
| EVENT_OCCURRED = 'EVENT_OCCURRED', |
| CLEAR_EVENTS = 'CLEAR_EVENTS', |
| } |
| |
| export interface DeviceBoundSessionModelEventTypes { |
| [DeviceBoundSessionModelEvents.INITIALIZE_SESSIONS]: {sessions: Protocol.Network.DeviceBoundSession[]}; |
| [DeviceBoundSessionModelEvents.ADD_VISIBLE_SITE]: {site: string}; |
| [DeviceBoundSessionModelEvents.CLEAR_VISIBLE_SITES]: void; |
| [DeviceBoundSessionModelEvents.EVENT_OCCURRED]: {site: string, sessionId?: string}; |
| [DeviceBoundSessionModelEvents.CLEAR_EVENTS]: { |
| emptySessions: Map<string, Array<string|undefined>>, |
| emptySites: Set<string>, |
| noLongerFailedSessions: Map<string, Array<string|undefined>>, |
| }; |
| } |