chrome-devtools-frontend
Version:
Chrome DevTools UI
221 lines (191 loc) • 8.25 kB
text/typescript
// 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>>,
};
}