UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

419 lines 16.9 kB
// Copyright 2023 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 { assertNotNullOrUndefined } from '../platform/platform.js'; import { SDKModel } from './SDKModel.js'; import { Capability } from './Target.js'; import { TargetManager } from './TargetManager.js'; import { Events as ResourceTreeModelEvents, ResourceTreeModel, } from './ResourceTreeModel.js'; // Holds preloading related information. // // - SpeculationRule rule sets // - Preloading attempts // - Relationship between rule sets and preloading attempts export class PreloadingModel extends SDKModel { agent; loaderIds = []; targetJustAttached = true; lastPrimaryPageModel = null; documents = new Map(); constructor(target) { super(target); target.registerPreloadDispatcher(new PreloadDispatcher(this)); this.agent = target.preloadAgent(); void this.agent.invoke_enable(); const targetInfo = target.targetInfo(); if (targetInfo !== undefined && targetInfo.subtype === 'prerender') { this.lastPrimaryPageModel = TargetManager.instance().primaryPageTarget()?.model(PreloadingModel) || null; } TargetManager.instance().addModelListener(ResourceTreeModel, ResourceTreeModelEvents.PrimaryPageChanged, this.onPrimaryPageChanged, this); } dispose() { super.dispose(); TargetManager.instance().removeModelListener(ResourceTreeModel, ResourceTreeModelEvents.PrimaryPageChanged, this.onPrimaryPageChanged, this); void this.agent.invoke_disable(); } ensureDocumentPreloadingData(loaderId) { if (this.documents.get(loaderId) === undefined) { this.documents.set(loaderId, new DocumentPreloadingData()); } } currentLoaderId() { // Target is just attached and didn't received CDP events that we can infer loaderId. if (this.targetJustAttached) { return null; } if (this.loaderIds.length === 0) { throw new Error('unreachable'); } return this.loaderIds[this.loaderIds.length - 1]; } currentDocument() { const loaderId = this.currentLoaderId(); return loaderId === null ? null : this.documents.get(loaderId) || null; } // Returns a rule set of the current page. // // Returns reference. Don't save returned values. // Returned value may or may not be updated as the time grows. getRuleSetById(id) { return this.currentDocument()?.ruleSets.getById(id) || null; } // Returns rule sets of the current page. // // Returns array of pairs of id and reference. Don't save returned references. // Returned values may or may not be updated as the time grows. getAllRuleSets() { return this.currentDocument()?.ruleSets.getAll() || []; } getPreloadCountsByRuleSetId() { const countsByRuleSetId = new Map(); for (const { value } of this.getPreloadingAttempts(null)) { for (const ruleSetId of [null, ...value.ruleSetIds]) { if (countsByRuleSetId.get(ruleSetId) === undefined) { countsByRuleSetId.set(ruleSetId, new Map()); } const countsByStatus = countsByRuleSetId.get(ruleSetId); assertNotNullOrUndefined(countsByStatus); const i = countsByStatus.get(value.status) || 0; countsByStatus.set(value.status, i + 1); } } return countsByRuleSetId; } // Returns a preloading attempt of the current page. // // Returns reference. Don't save returned values. // Returned value may or may not be updated as the time grows. getPreloadingAttemptById(id) { const document = this.currentDocument(); if (document === null) { return null; } return document.preloadingAttempts.getById(id, document.sources) || null; } // Returs preloading attempts of the current page that triggered by the rule set with `ruleSetId`. // `ruleSetId === null` means "do not filter". // // Returns array of pairs of id and reference. Don't save returned references. // Returned values may or may not be updated as the time grows. getPreloadingAttempts(ruleSetId) { const document = this.currentDocument(); if (document === null) { return []; } return document.preloadingAttempts.getAll(ruleSetId, document.sources); } // Returs preloading attempts of the previousPgae. // // Returns array of pairs of id and reference. Don't save returned references. // Returned values may or may not be updated as the time grows. getPreloadingAttemptsOfPreviousPage() { if (this.loaderIds.length <= 1) { return []; } const document = this.documents.get(this.loaderIds[this.loaderIds.length - 2]); if (document === undefined) { return []; } return document.preloadingAttempts.getAll(null, document.sources); } onPrimaryPageChanged(event) { const { frame, type } = event.data; // Model of prerendered page's target will hands over. Do nothing for the initiator page. if (this.lastPrimaryPageModel === null && type === "Activation" /* PrimaryPageChangeType.Activation */) { return; } if (this.lastPrimaryPageModel !== null && type !== "Activation" /* PrimaryPageChangeType.Activation */) { return; } if (this.lastPrimaryPageModel !== null && type === "Activation" /* PrimaryPageChangeType.Activation */) { // Hand over from the model of the last primary page. this.loaderIds = this.lastPrimaryPageModel.loaderIds; for (const [loaderId, prev] of this.lastPrimaryPageModel.documents.entries()) { this.ensureDocumentPreloadingData(loaderId); this.documents.get(loaderId)?.mergePrevious(prev); } } this.lastPrimaryPageModel = null; // Note that at this timing ResourceTreeFrame.loaderId is ensured to // be non empty and Protocol.Network.LoaderId because it is filled // by ResourceTreeFrame.navigate. const currentLoaderId = frame.loaderId; // Holds histories for two pages at most. this.loaderIds.push(currentLoaderId); this.loaderIds = this.loaderIds.slice(-2); this.ensureDocumentPreloadingData(currentLoaderId); for (const loaderId of this.documents.keys()) { if (!this.loaderIds.includes(loaderId)) { this.documents.delete(loaderId); } } this.dispatchEventToListeners(Events.ModelUpdated); } onRuleSetUpdated(event) { const ruleSet = event.ruleSet; const loaderId = ruleSet.loaderId; // Infer current loaderId if DevTools is opned at the current page. if (this.currentLoaderId() === null) { this.loaderIds = [loaderId]; this.targetJustAttached = false; } this.ensureDocumentPreloadingData(loaderId); this.documents.get(loaderId)?.ruleSets.upsert(ruleSet); this.dispatchEventToListeners(Events.ModelUpdated); } onRuleSetRemoved(event) { const id = event.id; for (const document of this.documents.values()) { document.ruleSets.delete(id); } this.dispatchEventToListeners(Events.ModelUpdated); } onPreloadingAttemptSourcesUpdated(event) { const loaderId = event.loaderId; this.ensureDocumentPreloadingData(loaderId); const document = this.documents.get(loaderId); if (document === undefined) { return; } document.sources.update(event.preloadingAttemptSources); document.preloadingAttempts.maybeRegisterNotTriggered(document.sources); this.dispatchEventToListeners(Events.ModelUpdated); } onPrefetchStatusUpdated(event) { const loaderId = event.key.loaderId; this.ensureDocumentPreloadingData(loaderId); const attempt = { action: "Prefetch" /* Protocol.Preload.SpeculationAction.Prefetch */, key: event.key, status: convertPreloadingStatus(event.status), prefetchStatus: event.prefetchStatus || null, requestId: event.requestId, }; this.documents.get(loaderId)?.preloadingAttempts.upsert(attempt); this.dispatchEventToListeners(Events.ModelUpdated); } onPrerenderStatusUpdated(event) { const loaderId = event.key.loaderId; this.ensureDocumentPreloadingData(loaderId); const attempt = { action: "Prerender" /* Protocol.Preload.SpeculationAction.Prerender */, key: event.key, status: convertPreloadingStatus(event.status), prerenderStatus: event.prerenderStatus || null, disallowedMojoInterface: event.disallowedMojoInterface || null, }; this.documents.get(loaderId)?.preloadingAttempts.upsert(attempt); this.dispatchEventToListeners(Events.ModelUpdated); } onPreloadEnabledStateUpdated(event) { this.dispatchEventToListeners(Events.WarningsUpdated, event); } } SDKModel.register(PreloadingModel, { capabilities: Capability.DOM, autostart: false }); // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export var Events; (function (Events) { Events["ModelUpdated"] = "ModelUpdated"; Events["WarningsUpdated"] = "WarningsUpdated"; })(Events || (Events = {})); class PreloadDispatcher { model; constructor(model) { this.model = model; } ruleSetUpdated(event) { this.model.onRuleSetUpdated(event); } ruleSetRemoved(event) { this.model.onRuleSetRemoved(event); } preloadingAttemptSourcesUpdated(event) { this.model.onPreloadingAttemptSourcesUpdated(event); } prefetchStatusUpdated(event) { this.model.onPrefetchStatusUpdated(event); } prerenderAttemptCompleted(_) { } prerenderStatusUpdated(event) { this.model.onPrerenderStatusUpdated(event); } preloadEnabledStateUpdated(event) { void this.model.onPreloadEnabledStateUpdated(event); } } class DocumentPreloadingData { ruleSets = new RuleSetRegistry(); preloadingAttempts = new PreloadingAttemptRegistry(); sources = new SourceRegistry(); mergePrevious(prev) { // Note that CDP events Preload.ruleSetUpdated/Deleted and // Preload.preloadingAttemptSourcesUpdated with a loaderId are emitted to target that bounded to // a document with the loaderId. On the other hand, prerendering activation changes targets // of Preload.prefetch/prerenderStatusUpdated, i.e. activated page receives those events for // triggering outcome "Success". if (!this.ruleSets.isEmpty() || !this.sources.isEmpty()) { throw new Error('unreachable'); } this.ruleSets = prev.ruleSets; this.preloadingAttempts.mergePrevious(prev.preloadingAttempts); this.sources = prev.sources; } } class RuleSetRegistry { map = new Map(); isEmpty() { return this.map.size === 0; } // Returns reference. Don't save returned values. // Returned values may or may not be updated as the time grows. getById(id) { return this.map.get(id) || null; } // Returns reference. Don't save returned values. // Returned values may or may not be updated as the time grows. getAll() { return Array.from(this.map.entries()).map(([id, value]) => ({ id, value })); } upsert(ruleSet) { this.map.set(ruleSet.id, ruleSet); } delete(id) { this.map.delete(id); } } function convertPreloadingStatus(status) { switch (status) { case "Pending" /* Protocol.Preload.PreloadingStatus.Pending */: return "Pending" /* PreloadingStatus.Pending */; case "Running" /* Protocol.Preload.PreloadingStatus.Running */: return "Running" /* PreloadingStatus.Running */; case "Ready" /* Protocol.Preload.PreloadingStatus.Ready */: return "Ready" /* PreloadingStatus.Ready */; case "Success" /* Protocol.Preload.PreloadingStatus.Success */: return "Success" /* PreloadingStatus.Success */; case "Failure" /* Protocol.Preload.PreloadingStatus.Failure */: return "Failure" /* PreloadingStatus.Failure */; case "NotSupported" /* Protocol.Preload.PreloadingStatus.NotSupported */: return "NotSupported" /* PreloadingStatus.NotSupported */; } throw new Error('unreachable'); } function makePreloadingAttemptId(key) { let action; switch (key.action) { case "Prefetch" /* Protocol.Preload.SpeculationAction.Prefetch */: action = 'Prefetch'; break; case "Prerender" /* Protocol.Preload.SpeculationAction.Prerender */: action = 'Prerender'; break; } let targetHint; switch (key.targetHint) { case undefined: targetHint = 'undefined'; break; case "Blank" /* Protocol.Preload.SpeculationTargetHint.Blank */: targetHint = 'Blank'; break; case "Self" /* Protocol.Preload.SpeculationTargetHint.Self */: targetHint = 'Self'; break; } return `${key.loaderId}:${action}:${key.url}:${targetHint}`; } class PreloadingAttemptRegistry { map = new Map(); enrich(attempt, source) { let ruleSetIds = []; let nodeIds = []; if (source !== null) { ruleSetIds = source.ruleSetIds; nodeIds = source.nodeIds; } return { ...attempt, ruleSetIds, nodeIds, }; } // Returns reference. Don't save returned values. // Returned values may or may not be updated as the time grows. getById(id, sources) { const attempt = this.map.get(id) || null; if (attempt === null) { return null; } return this.enrich(attempt, sources.getById(id)); } // Returs preloading attempts that triggered by the rule set with `ruleSetId`. // `ruleSetId === null` means "do not filter". // // Returns reference. Don't save returned values. // Returned values may or may not be updated as the time grows. getAll(ruleSetId, sources) { return [...this.map.entries()] .map(([id, value]) => ({ id, value: this.enrich(value, sources.getById(id)) })) .filter(({ value }) => !ruleSetId || value.ruleSetIds.includes(ruleSetId)); } upsert(attempt) { const id = makePreloadingAttemptId(attempt.key); this.map.set(id, attempt); } maybeRegisterNotTriggered(sources) { for (const [id, { key }] of sources.entries()) { if (this.map.get(id) !== undefined) { continue; } let attempt; switch (key.action) { case "Prefetch" /* Protocol.Preload.SpeculationAction.Prefetch */: attempt = { action: "Prefetch" /* Protocol.Preload.SpeculationAction.Prefetch */, key, status: "NotTriggered" /* PreloadingStatus.NotTriggered */, prefetchStatus: null, // Fill invalid request id. requestId: '', }; break; case "Prerender" /* Protocol.Preload.SpeculationAction.Prerender */: attempt = { action: "Prerender" /* Protocol.Preload.SpeculationAction.Prerender */, key, status: "NotTriggered" /* PreloadingStatus.NotTriggered */, prerenderStatus: null, disallowedMojoInterface: null, }; break; } this.map.set(id, attempt); } } mergePrevious(prev) { for (const [id, attempt] of this.map.entries()) { prev.map.set(id, attempt); } this.map = prev.map; } } class SourceRegistry { map = new Map(); entries() { return this.map.entries(); } isEmpty() { return this.map.size === 0; } getById(id) { return this.map.get(id) || null; } update(sources) { this.map = new Map(sources.map(s => [makePreloadingAttemptId(s.key), s])); } } //# sourceMappingURL=PreloadingModel.js.map