| /** |
| * @license |
| * Copyright 2022 Google Inc. |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| import type {Frame} from '../api/Frame.js'; |
| import {Deferred} from '../util/Deferred.js'; |
| |
| /** |
| * Keeps track of the page frame tree and it's is managed by |
| * {@link FrameManager}. FrameTree uses frame IDs to reference frame and it |
| * means that referenced frames might not be in the tree anymore. Thus, the tree |
| * structure is eventually consistent. |
| * @internal |
| */ |
| export class FrameTree<FrameType extends Frame> { |
| #frames = new Map<string, FrameType>(); |
| // frameID -> parentFrameID |
| #parentIds = new Map<string, string>(); |
| // frameID -> childFrameIDs |
| #childIds = new Map<string, Set<string>>(); |
| #mainFrame?: FrameType; |
| #isMainFrameStale = false; |
| #waitRequests = new Map<string, Set<Deferred<FrameType>>>(); |
| |
| getMainFrame(): FrameType | undefined { |
| return this.#mainFrame; |
| } |
| |
| getById(frameId: string): FrameType | undefined { |
| return this.#frames.get(frameId); |
| } |
| |
| /** |
| * Returns a promise that is resolved once the frame with |
| * the given ID is added to the tree. |
| */ |
| waitForFrame(frameId: string): Promise<FrameType> { |
| const frame = this.getById(frameId); |
| if (frame) { |
| return Promise.resolve(frame); |
| } |
| const deferred = Deferred.create<FrameType>(); |
| const callbacks = |
| this.#waitRequests.get(frameId) || new Set<Deferred<FrameType>>(); |
| callbacks.add(deferred); |
| return deferred.valueOrThrow(); |
| } |
| |
| frames(): FrameType[] { |
| return Array.from(this.#frames.values()); |
| } |
| |
| addFrame(frame: FrameType): void { |
| this.#frames.set(frame._id, frame); |
| if (frame._parentId) { |
| this.#parentIds.set(frame._id, frame._parentId); |
| if (!this.#childIds.has(frame._parentId)) { |
| this.#childIds.set(frame._parentId, new Set()); |
| } |
| this.#childIds.get(frame._parentId)!.add(frame._id); |
| } else if (!this.#mainFrame || this.#isMainFrameStale) { |
| this.#mainFrame = frame; |
| this.#isMainFrameStale = false; |
| } |
| this.#waitRequests.get(frame._id)?.forEach(request => { |
| return request.resolve(frame); |
| }); |
| } |
| |
| removeFrame(frame: FrameType): void { |
| this.#frames.delete(frame._id); |
| this.#parentIds.delete(frame._id); |
| if (frame._parentId) { |
| this.#childIds.get(frame._parentId)?.delete(frame._id); |
| } else { |
| this.#isMainFrameStale = true; |
| } |
| } |
| |
| childFrames(frameId: string): FrameType[] { |
| const childIds = this.#childIds.get(frameId); |
| if (!childIds) { |
| return []; |
| } |
| return Array.from(childIds) |
| .map(id => { |
| return this.getById(id); |
| }) |
| .filter((frame): frame is FrameType => { |
| return frame !== undefined; |
| }); |
| } |
| |
| parentFrame(frameId: string): FrameType | undefined { |
| const parentId = this.#parentIds.get(frameId); |
| return parentId ? this.getById(parentId) : undefined; |
| } |
| } |