@quick-game/cli
Version:
Command line interface for rapid qg development
375 lines • 14.9 kB
JavaScript
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../common/common.js';
import * as Platform from '../platform/platform.js';
import { Type as TargetType, Target } from './Target.js';
import { SDKModel } from './SDKModel.js';
import * as Root from '../root/root.js';
import * as Host from '../host/host.js';
import { assertNotNullOrUndefined } from '../platform/platform.js';
let targetManagerInstance;
export class TargetManager extends Common.ObjectWrapper.ObjectWrapper {
#targetsInternal;
#observers;
/* eslint-disable @typescript-eslint/no-explicit-any */
#modelListeners;
#modelObservers;
#scopedObservers;
/* eslint-enable @typescript-eslint/no-explicit-any */
#isSuspended;
#browserTargetInternal;
#scopeTarget;
#defaultScopeSet;
#scopeChangeListeners;
constructor() {
super();
this.#targetsInternal = new Set();
this.#observers = new Set();
this.#modelListeners = new Platform.MapUtilities.Multimap();
this.#modelObservers = new Platform.MapUtilities.Multimap();
this.#isSuspended = false;
this.#browserTargetInternal = null;
this.#scopeTarget = null;
this.#scopedObservers = new WeakSet();
this.#defaultScopeSet = false;
this.#scopeChangeListeners = new Set();
}
static instance({ forceNew } = { forceNew: false }) {
if (!targetManagerInstance || forceNew) {
targetManagerInstance = new TargetManager();
}
return targetManagerInstance;
}
static removeInstance() {
targetManagerInstance = undefined;
}
onInspectedURLChange(target) {
if (target !== this.#scopeTarget) {
return;
}
Host.InspectorFrontendHost.InspectorFrontendHostInstance.inspectedURLChanged(target.inspectedURL() || Platform.DevToolsPath.EmptyUrlString);
this.dispatchEventToListeners(Events.InspectedURLChanged, target);
}
onNameChange(target) {
this.dispatchEventToListeners(Events.NameChanged, target);
}
async suspendAllTargets(reason) {
if (this.#isSuspended) {
return;
}
this.#isSuspended = true;
this.dispatchEventToListeners(Events.SuspendStateChanged);
const suspendPromises = Array.from(this.#targetsInternal.values(), target => target.suspend(reason));
await Promise.all(suspendPromises);
}
async resumeAllTargets() {
if (!this.#isSuspended) {
return;
}
this.#isSuspended = false;
this.dispatchEventToListeners(Events.SuspendStateChanged);
const resumePromises = Array.from(this.#targetsInternal.values(), target => target.resume());
await Promise.all(resumePromises);
}
allTargetsSuspended() {
return this.#isSuspended;
}
models(modelClass, opts) {
const result = [];
for (const target of this.#targetsInternal) {
if (opts?.scoped && !this.isInScope(target)) {
continue;
}
const model = target.model(modelClass);
if (!model) {
continue;
}
result.push(model);
}
return result;
}
inspectedURL() {
const mainTarget = this.primaryPageTarget();
return mainTarget ? mainTarget.inspectedURL() : '';
}
observeModels(modelClass, observer, opts) {
const models = this.models(modelClass, opts);
this.#modelObservers.set(modelClass, observer);
if (opts?.scoped) {
this.#scopedObservers.add(observer);
}
for (const model of models) {
observer.modelAdded(model);
}
}
unobserveModels(modelClass, observer) {
this.#modelObservers.delete(modelClass, observer);
this.#scopedObservers.delete(observer);
}
modelAdded(target, modelClass, model, inScope) {
for (const observer of this.#modelObservers.get(modelClass).values()) {
if (!this.#scopedObservers.has(observer) || inScope) {
observer.modelAdded(model);
}
}
}
modelRemoved(target, modelClass, model, inScope) {
for (const observer of this.#modelObservers.get(modelClass).values()) {
if (!this.#scopedObservers.has(observer) || inScope) {
observer.modelRemoved(model);
}
}
}
addModelListener(modelClass, eventType, listener, thisObject, opts) {
const wrappedListener = (event) => {
if (!opts?.scoped || this.isInScope(event)) {
listener.call(thisObject, event);
}
};
for (const model of this.models(modelClass)) {
model.addEventListener(eventType, wrappedListener);
}
this.#modelListeners.set(eventType, { modelClass, thisObject, listener, wrappedListener });
}
removeModelListener(modelClass, eventType, listener, thisObject) {
if (!this.#modelListeners.has(eventType)) {
return;
}
let wrappedListener = null;
for (const info of this.#modelListeners.get(eventType)) {
if (info.modelClass === modelClass && info.listener === listener && info.thisObject === thisObject) {
wrappedListener = info.wrappedListener;
this.#modelListeners.delete(eventType, info);
}
}
if (wrappedListener) {
for (const model of this.models(modelClass)) {
model.removeEventListener(eventType, wrappedListener);
}
}
}
observeTargets(targetObserver, opts) {
if (this.#observers.has(targetObserver)) {
throw new Error('Observer can only be registered once');
}
if (opts?.scoped) {
this.#scopedObservers.add(targetObserver);
}
for (const target of this.#targetsInternal) {
if (!opts?.scoped || this.isInScope(target)) {
targetObserver.targetAdded(target);
}
}
this.#observers.add(targetObserver);
}
unobserveTargets(targetObserver) {
this.#observers.delete(targetObserver);
this.#scopedObservers.delete(targetObserver);
}
createTarget(id, name, type, parentTarget, sessionId, waitForDebuggerInPage, connection, targetInfo) {
const target = new Target(this, id, name, type, parentTarget, sessionId || '', this.#isSuspended, connection || null, targetInfo);
if (waitForDebuggerInPage) {
void target.pageAgent().invoke_waitForDebugger();
}
target.createModels(new Set(this.#modelObservers.keysArray()));
this.#targetsInternal.add(target);
const inScope = this.isInScope(target);
// Iterate over a copy. #observers might be modified during iteration.
for (const observer of [...this.#observers]) {
if (!this.#scopedObservers.has(observer) || inScope) {
observer.targetAdded(target);
}
}
for (const [modelClass, model] of target.models().entries()) {
this.modelAdded(target, modelClass, model, inScope);
}
for (const key of this.#modelListeners.keysArray()) {
for (const info of this.#modelListeners.get(key)) {
const model = target.model(info.modelClass);
if (model) {
model.addEventListener(key, info.wrappedListener);
}
}
}
if ((target === target.outermostTarget() &&
(target.type() !== TargetType.Frame || target === this.primaryPageTarget())) &&
!this.#defaultScopeSet) {
this.setScopeTarget(target);
}
return target;
}
removeTarget(target) {
if (!this.#targetsInternal.has(target)) {
return;
}
const inScope = this.isInScope(target);
this.#targetsInternal.delete(target);
for (const modelClass of target.models().keys()) {
const model = target.models().get(modelClass);
assertNotNullOrUndefined(model);
this.modelRemoved(target, modelClass, model, inScope);
}
// Iterate over a copy. #observers might be modified during iteration.
for (const observer of [...this.#observers]) {
if (!this.#scopedObservers.has(observer) || inScope) {
observer.targetRemoved(target);
}
}
for (const key of this.#modelListeners.keysArray()) {
for (const info of this.#modelListeners.get(key)) {
const model = target.model(info.modelClass);
if (model) {
model.removeEventListener(key, info.wrappedListener);
}
}
}
}
targets() {
return [...this.#targetsInternal];
}
targetById(id) {
// TODO(dgozman): add a map #id -> #target.
return this.targets().find(target => target.id() === id) || null;
}
rootTarget() {
return this.#targetsInternal.size ? this.#targetsInternal.values().next().value : null;
}
primaryPageTarget() {
let target = this.rootTarget();
if (target?.type() === TargetType.Tab) {
target =
this.targets().find(t => t.parentTarget() === target && t.type() === TargetType.Frame && !t.targetInfo()?.subtype?.length) ||
null;
}
return target;
}
browserTarget() {
return this.#browserTargetInternal;
}
async maybeAttachInitialTarget() {
if (!Boolean(Root.Runtime.Runtime.queryParam('browserConnection'))) {
return false;
}
if (!this.#browserTargetInternal) {
this.#browserTargetInternal = new Target(this, /* #id*/ 'main', /* #name*/ 'browser', TargetType.Browser, /* #parentTarget*/ null,
/* #sessionId */ '', /* suspended*/ false, /* #connection*/ null, /* targetInfo*/ undefined);
this.#browserTargetInternal.createModels(new Set(this.#modelObservers.keysArray()));
}
const targetId = await Host.InspectorFrontendHost.InspectorFrontendHostInstance.initialTargetId();
// Do not await for Target.autoAttachRelated to return, as it goes throguh the renderer and we don't want to block early
// at front-end initialization if a renderer is stuck. The rest of #target discovery and auto-attach process should happen
// asynchronously upon Target.attachedToTarget.
void this.#browserTargetInternal.targetAgent().invoke_autoAttachRelated({
targetId,
waitForDebuggerOnStart: true,
});
return true;
}
clearAllTargetsForTest() {
this.#targetsInternal.clear();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isInScope(arg) {
if (!arg) {
return false;
}
if (isSDKModelEvent(arg)) {
arg = arg.source;
}
if (arg instanceof SDKModel) {
arg = arg.target();
}
while (arg && arg !== this.#scopeTarget) {
arg = arg.parentTarget();
}
return Boolean(arg) && arg === this.#scopeTarget;
}
// Sets a root of a scope substree.
// TargetManager API invoked with `scoped: true` will behave as if targets
// outside of the scope subtree don't exist. Concretely this means that
// target observers, model observers and model listeners won't be invoked for targets outside of the
// scope tree. This method will invoke targetRemoved and modelRemoved for
// objects in the previous scope, as if they disappear and then will invoke
// targetAdded and modelAdded as if they just appeared.
// Note that scopeTarget could be null, which will effectively prevent scoped
// observes from getting any events.
setScopeTarget(scopeTarget) {
if (scopeTarget === this.#scopeTarget) {
return;
}
for (const target of this.targets()) {
if (!this.isInScope(target)) {
continue;
}
for (const modelClass of this.#modelObservers.keysArray()) {
const model = target.models().get(modelClass);
if (!model) {
continue;
}
for (const observer of [...this.#modelObservers.get(modelClass)].filter(o => this.#scopedObservers.has(o))) {
observer.modelRemoved(model);
}
}
// Iterate over a copy. #observers might be modified during iteration.
for (const observer of [...this.#observers].filter(o => this.#scopedObservers.has(o))) {
observer.targetRemoved(target);
}
}
this.#scopeTarget = scopeTarget;
for (const target of this.targets()) {
if (!this.isInScope(target)) {
continue;
}
for (const observer of [...this.#observers].filter(o => this.#scopedObservers.has(o))) {
observer.targetAdded(target);
}
for (const [modelClass, model] of target.models().entries()) {
for (const observer of [...this.#modelObservers.get(modelClass)].filter(o => this.#scopedObservers.has(o))) {
observer.modelAdded(model);
}
}
}
for (const scopeChangeListener of this.#scopeChangeListeners) {
scopeChangeListener();
}
if (scopeTarget && scopeTarget.inspectedURL()) {
this.onInspectedURLChange(scopeTarget);
}
}
addScopeChangeListener(listener) {
this.#scopeChangeListeners.add(listener);
}
removeScopeChangeListener(listener) {
this.#scopeChangeListeners.delete(listener);
}
scopeTarget() {
return this.#scopeTarget;
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export var Events;
(function (Events) {
Events["AvailableTargetsChanged"] = "AvailableTargetsChanged";
Events["InspectedURLChanged"] = "InspectedURLChanged";
Events["NameChanged"] = "NameChanged";
Events["SuspendStateChanged"] = "SuspendStateChanged";
})(Events || (Events = {}));
export class Observer {
targetAdded(_target) {
}
targetRemoved(_target) {
}
}
export class SDKModelObserver {
modelAdded(_model) {
}
modelRemoved(_model) {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isSDKModelEvent(arg) {
return 'source' in arg && arg.source instanceof SDKModel;
}
//# sourceMappingURL=TargetManager.js.map