diff --git a/src/chrome/DebuggerPageDomain.ts b/src/chrome/DebuggerPageDomain.ts new file mode 100644 index 0000000..0cc8a23 --- /dev/null +++ b/src/chrome/DebuggerPageDomain.ts @@ -0,0 +1,30 @@ +/** + * @file + * Strings passed to `chrome.debugger.sendCommand` and received from + * `chrome.debugger.onEvent` callbacks. + */ + +import {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping'; + +/** @see https://chromedevtools.github.io/devtools-protocol/tot/Page/#methods */ +export enum PageDebuggerMethod { + disable = 'Page.disable', + enable = 'Page.enable', +} + +/** @see https://chromedevtools.github.io/devtools-protocol/tot/Page/#events */ +export enum PageDebuggerEvent { + domContentEventFired = 'Page.domContentEventFired', + frameAttached = 'Page.frameAttached', + frameDetached = 'Page.frameDetached', + frameNavigated = 'Page.frameNavigated', + frameRequestedNavigation = 'Page.frameRequestedNavigation', + frameStartedLoading = 'Page.frameStartedLoading', + frameStoppedLoading = 'Page.frameStoppedLoading', + lifecycleEvent = 'Page.lifecycleEvent', + loadEventFired = 'Page.loadEventFired', +} + +/** @see https://chromedevtools.github.io/devtools-protocol/tot/Page/#types */ +export type PageDebuggerEventParams = + ProtocolMapping.Events[Name]; diff --git a/src/devtools.html b/src/devtools.html index afac880..28c4271 100644 --- a/src/devtools.html +++ b/src/devtools.html @@ -3,6 +3,6 @@ DevTools: Audion Extension - + diff --git a/src/devtools/DebuggerAttachEventController.ts b/src/devtools/DebuggerAttachEventController.ts index ae8101d..ff99715 100644 --- a/src/devtools/DebuggerAttachEventController.ts +++ b/src/devtools/DebuggerAttachEventController.ts @@ -7,6 +7,7 @@ import { Observable, of, Subject, + Subscriber, } from 'rxjs'; import { catchError, @@ -21,6 +22,7 @@ import { } from 'rxjs/operators'; import {chrome} from '../chrome'; +import {PageDebuggerMethod} from '../chrome/DebuggerPageDomain'; import {WebAudioDebuggerMethod} from '../chrome/DebuggerWebAudioDomain'; /** @@ -80,6 +82,8 @@ export interface DebuggerAttachEventState { permission: AttachPermission; attachInterest: number; attachState: BinaryTransition; + pageEventInterest: number; + pageEventState: BinaryTransition; webAudioEventInterest: number; webAudioEventState: BinaryTransition; } @@ -120,6 +124,12 @@ export class DebuggerAttachEventController { /** How many subscriptions want to attach to `chrome.debugger`. */ attachInterest$: CounterSubject; attachState$: Observable; + /** + * How many subscriptions want to receive page events through + * `chrome.debugger.onEvent`. + */ + pageEventInterest$: CounterSubject; + pageEventState$: Observable; /** * How many subscriptions want to receive web audio events through * `chrome.debugger.onEvent`. @@ -146,6 +156,15 @@ export class DebuggerAttachEventController { activateAction: () => attach({tabId}, debuggerVersion), deactivateAction: () => detach({tabId}), }), + // How many entities want to listen to page events through `onEvent`. + pageEventInterest: new CounterSubject(0), + // must be IS_ACTIVE for `onEvent` to receive events. + pageEventState: new BinaryTransitionSubject({ + initialState: BinaryTransition.IS_INACTIVE, + activateAction: () => sendCommand({tabId}, PageDebuggerMethod.enable), + deactivateAction: () => + sendCommand({tabId}, PageDebuggerMethod.disable), + }), // How many entities want to listen to web audio events through `onEvent`. webAudioEventInterest: new CounterSubject(0), // webAudioEventState must be IS_ACTIVE for `onEvent` to receive events. @@ -160,6 +179,8 @@ export class DebuggerAttachEventController { this.permission$ = debuggerSubject.permission; this.attachInterest$ = debuggerSubject.attachInterest; this.attachState$ = debuggerSubject.attachState; + this.pageEventInterest$ = debuggerSubject.pageEventInterest; + this.pageEventState$ = debuggerSubject.pageEventState; this.webAudioEventInterest$ = debuggerSubject.webAudioEventInterest; this.webAudioEventState$ = debuggerSubject.webAudioEventState; @@ -174,6 +195,8 @@ export class DebuggerAttachEventController { previous.permission === current.permission && previous.attachInterest === current.attachInterest && previous.attachState === current.attachState && + previous.pageEventInterest === current.pageEventInterest && + previous.pageEventState === current.pageEventState && previous.webAudioEventInterest === current.webAudioEventInterest && previous.webAudioEventState === current.webAudioEventState, ), @@ -225,33 +248,19 @@ export class DebuggerAttachEventController { }, }); - // Govern receiving web audio events through `chrome.debugger.onEvent`. - debuggerState$.subscribe({ - next(state) { - if ( - state.attachState === BinaryTransition.IS_ACTIVE && - state.webAudioEventInterest > 0 - ) { - // Start receiving events. The attachemnt is active and some entities - // are listeneing for events. - debuggerSubject.webAudioEventState.activate(); - } else { - if (state.attachState === BinaryTransition.IS_ACTIVE) { - // Stop receiving events. The attachment is still active but no - // entities are listening for events. - debuggerSubject.webAudioEventState.deactivate(); - } else { - // "Skip" deactivation of receiving events and immediately go to - // the inactive state. The process of detachment either requested by - // the extension or initiated otherwise has implicitly stopped - // reception of events. - debuggerSubject.webAudioEventState.next( - BinaryTransition.IS_INACTIVE, - ); - } - } - }, - }); + // Govern receiving events through `chrome.debugger.onEvent`. + debuggerState$.subscribe( + activateEventWhileAttached( + debuggerSubject.pageEventState, + ({pageEventInterest}) => pageEventInterest > 0, + ), + ); + debuggerState$.subscribe( + activateEventWhileAttached( + debuggerSubject.webAudioEventState, + ({webAudioEventInterest}) => webAudioEventInterest > 0, + ), + ); } /** @@ -270,6 +279,36 @@ export class DebuggerAttachEventController { } } +function activateEventWhileAttached( + eventState: BinaryTransitionSubject, + interestExists: (state: DebuggerAttachEventState) => boolean, +): Partial> { + return { + next(state) { + if ( + state.attachState === BinaryTransition.IS_ACTIVE && + interestExists(state) + ) { + // Start receiving events. The attachemnt is active and some entities + // are listening for events. + eventState.activate(); + } else { + if (state.attachState === BinaryTransition.IS_ACTIVE) { + // Stop receiving events. The attachment is still active but no + // entities are listening for events. + eventState.deactivate(); + } else { + // "Skip" deactivation of receiving events and immediately go to the + // inactive state. The process of detachment either requested by the + // extension or initiated otherwise has implicitly stopped reception + // of events. + eventState.next(BinaryTransition.IS_INACTIVE); + } + } + }, + }; +} + /** * Create a function that returns an observable that completes when the api * calls back. diff --git a/src/devtools/DebuggerEvents.ts b/src/devtools/DebuggerEvents.ts new file mode 100644 index 0000000..7d5a0f0 --- /dev/null +++ b/src/devtools/DebuggerEvents.ts @@ -0,0 +1,44 @@ +import {filter, map, Observable} from 'rxjs'; +import {chrome} from '../chrome'; +import {fromChromeEvent} from '../utils/rxChrome'; +import {DebuggerAttachEventController} from './DebuggerAttachEventController'; +import {Audion} from './Types'; + +type DebuggerDomain = 'page' | 'webAudio'; + +interface DebuggerEventsOptions { + domain: D; +} + +type DebuggerDomainEvent = D extends 'page' + ? Audion.PageEvent + : D extends 'webAudio' + ? Audion.WebAudioEvent + : never; + +export class DebuggerEventsObservable< + D extends DebuggerDomain, +> extends Observable> { + constructor( + public attachController: DebuggerAttachEventController, + public options: DebuggerEventsOptions, + ) { + super((subscriber) => { + attachController.attachInterest$.increment(); + attachController[options.domain + 'EventInterest$'].increment(); + const subscription = fromChromeEvent(chrome.debugger.onEvent) + .pipe( + map(([debuggeeId, method, params]) => ({method, params})), + filter(({method}) => + method.toLowerCase().startsWith(options.domain.toLowerCase()), + ), + ) + .subscribe(subscriber); + subscription.add(() => { + attachController.attachInterest$.decrement(); + attachController[options.domain + 'EventInterest$'].decrement(); + }); + return subscription; + }); + } +} diff --git a/src/devtools/Types.ts b/src/devtools/Types.ts index 3631331..f151c76 100644 --- a/src/devtools/Types.ts +++ b/src/devtools/Types.ts @@ -1,6 +1,10 @@ /// import {Protocol} from 'devtools-protocol/types/protocol'; +import { + PageDebuggerEvent, + PageDebuggerEventParams, +} from '../chrome/DebuggerPageDomain'; import { WebAudioDebuggerEvent, @@ -89,6 +93,11 @@ export namespace Audion { edges: Protocol.WebAudio.NodesConnectedEvent[]; } + export type PageEvent = { + method: N; + params: PageDebuggerEventParams[0]; + }; + export type WebAudioEvent< N extends WebAudioDebuggerEvent = WebAudioDebuggerEvent, > = { diff --git a/src/devtools/WebAudioGraphIntegrator.ts b/src/devtools/WebAudioGraphIntegrator.ts index 5e920b1..a15552e 100644 --- a/src/devtools/WebAudioGraphIntegrator.ts +++ b/src/devtools/WebAudioGraphIntegrator.ts @@ -20,6 +20,8 @@ import { takeUntil, take, ignoreElements, + finalize, + share, } from 'rxjs/operators'; import {WebAudioDebuggerEvent} from '../chrome/DebuggerWebAudioDomain'; @@ -29,16 +31,26 @@ import { INITIAL_CONTEXT_REALTIME_DATA, RealtimeDataErrorMessage, WebAudioRealtimeData, + WebAudioRealtimeDataReason, } from './WebAudioRealtimeData'; import { ChromeDebuggerAPIEventName, ChromeDebuggerAPIEvent, } from './DebuggerAttachEventController'; +import { + PageDebuggerEvent, + PageDebuggerEventParams, +} from '../chrome/DebuggerPageDomain'; + +enum GraphContextDestroyReasonMessage { + RECEIVE_WILL_DESTROY_EVENT = 'ReceiveWillDestroyEvent', + CANNOT_FIND_REALTIME_DATA = 'CannotFindRealtimeData', +} type MutableContexts = { [key: string]: { graphContext: Audion.GraphContext; - graphContextDestroyed$: Subject; + graphContextDestroyed$: Subject; realtimeDataGraphContext$: Observable; }; }; @@ -47,9 +59,15 @@ interface EventHelpers { realtimeData: WebAudioRealtimeData; } -type IntegratableEventName = WebAudioDebuggerEvent | ChromeDebuggerAPIEventName; +type IntegratableEventName = + | PageDebuggerEvent + | WebAudioDebuggerEvent + | ChromeDebuggerAPIEventName; -type IntegratableEvent = Audion.WebAudioEvent | ChromeDebuggerAPIEvent; +type IntegratableEvent = + | Audion.PageEvent + | Audion.WebAudioEvent + | ChromeDebuggerAPIEvent; type IntegratableEventMapping = { [K in IntegratableEventName]: ProtocolMapping.Events extends { @@ -203,6 +221,13 @@ const EVENT_HANDLERS: Partial = { const {contextId} = contextChanged.context; const space = contexts[contextId]; if (!space) { + console.warn( + `Unexpected ${ + WebAudioDebuggerEvent.contextChanged + } event. Did not receive an event when Audio Context ${contextId.slice( + -6, + )} was created.`, + ); return; } @@ -224,6 +249,12 @@ const EVENT_HANDLERS: Partial = { contextCreated, ); return; + } else { + console.debug( + `Audio Context (${contextId.slice( + -6, + )}-${contextType}) created. Adding the context to the tracked set.`, + ); } const graph = new dagre.graphlib.Graph({multigraph: true}); @@ -232,11 +263,61 @@ const EVENT_HANDLERS: Partial = { return {}; }); - const realtimeData$ = - contextType === 'realtime' - ? helpers.realtimeData.pollContext(contextId) - : NEVER; - const graphContextDestroyed$ = new Subject(); + // Request realtime data for realtime and offline contexts. We use this + // information to help confirm the existence of this new context. Events + // that normally mark when contexts are destroyed may not arrive and so we + // need this extra way to determine when the contexts no longer exist. + const realtimeData$ = helpers.realtimeData.pollContext(contextId); + const graphContextDestroyed$ = + new Subject(); + + const realtimeDataGraphContext$ = realtimeData$.pipe( + map((realtimeData) => { + const space = contexts[contextId]; + if (space) { + space.graphContext = { + ...space.graphContext, + realtimeData, + }; + return space.graphContext; + } + }), + filter((context): context is Audion.GraphContext => Boolean(context)), + catchError((reason, caught) => { + reason = WebAudioRealtimeDataReason.parseReason(reason); + + if (WebAudioRealtimeDataReason.isCannotFindReason(reason)) { + const space = contexts[contextId]; + space?.graphContextDestroyed$?.next( + GraphContextDestroyReasonMessage.CANNOT_FIND_REALTIME_DATA, + ); + + if (!space) { + console.warn( + `Error requesting realtime data for context '${contextId}'. +Context was likely cleaned up during request for realtime data. +"${reason.message}"`, + ); + } + + return EMPTY; + } else if (WebAudioRealtimeDataReason.isRealtimeOnlyReason(reason)) { + // Non-realtime/offline contexts do not have realtime data and will + // produce this error when that data is requested. + } else { + console.error( + `Unexpected error requesting realtime data for context '${contextId}'. +"${WebAudioRealtimeDataReason.toString(reason)}"`, + ); + } + + // Redirect back to the caught observable. We want to keep receiving + // realtime data values or errors until we receive CANNOT_FIND error. + return caught; + }), + + takeUntil(graphContextDestroyed$), + ); contexts[contextId] = { graphContext: { @@ -252,69 +333,57 @@ const EVENT_HANDLERS: Partial = { graph: graph as unknown as graphlib.Graph, }, graphContextDestroyed$, - realtimeDataGraphContext$: realtimeData$.pipe( - map((realtimeData) => { - const space = contexts[contextId]; - if (space) { - space.graphContext = { - ...space.graphContext, - realtimeData, - }; - return space.graphContext; - } - }), - filter((context): context is Audion.GraphContext => Boolean(context)), - catchError((reason) => { - if (reason && reason.message && !reason.code) { - try { - reason = JSON.parse(reason.message); - } catch (e) {} - } + realtimeDataGraphContext$, + }; + return merge( + of(contexts[contextId].graphContext), + graphContextDestroyed$.pipe( + share(), + take(1), + mergeMap((message) => { if ( - reason && - reason.message === RealtimeDataErrorMessage.CANNOT_FIND + message === + GraphContextDestroyReasonMessage.CANNOT_FIND_REALTIME_DATA ) { - const space = contexts[contextId]; - if (space) { - delete contexts[contextId]; - return of({ - id: contextId, - eventCount: space?.graphContext?.eventCount + 1, - context: null, - realtimeData: null, - nodes: null, - params: null, - graph: null, - }); - } else { - console.warn( - `Error requesting realtime data for context '${contextId}'. -Context was likely cleaned up during request for realtime data. -"${reason.message}"`, - ); - } + console.debug( + `Audio Context (${contextId.slice( + -6, + )}-${contextType}) cannot be found. Removing the context from the tracked set.`, + ); } else if ( - reason && - reason.message === RealtimeDataErrorMessage.REALTIME_ONLY + message === + GraphContextDestroyReasonMessage.RECEIVE_WILL_DESTROY_EVENT ) { - console.error(`Error requesting realtime data for context '${contextId}'. -Context is of type '${contextCreated.context.contextType}' but should be 'realtime'. -"${reason.message}"`); + console.debug( + `Audio Context (${contextId.slice( + -6, + )}-${contextType}) will be destroyed. Removing the context from the tracked set.`, + ); + } + + const space = contexts[contextId]; + if (space) { + delete contexts[contextId]; + return of({ + id: contextId, + eventCount: space.graphContext?.eventCount + 1, + context: null, + realtimeData: null, + nodes: null, + params: null, + graph: null, + }); } else { - console.error( - `Unknown error requesting realtime data for context '${contextId}'. -"${reason && reason.message ? reason.message : reason}"`, + console.warn( + `Audio Context (${contextId.slice( + -6, + )}-${contextType}) could not be removed from tracked set. It was not tracked.`, ); } return EMPTY; }), - takeUntil(graphContextDestroyed$), ), - }; - - return merge( - of(contexts[contextId].graphContext), contexts[contextId].realtimeDataGraphContext$, ); }, @@ -326,19 +395,9 @@ Context is of type '${contextCreated.context.contextType}' but should be 'realti ) => { const {contextId} = contextDestroyed; const space = contexts[contextId]; - delete contexts[contextId]; - - space?.graphContextDestroyed$?.next(); - - return { - id: contextId, - eventCount: space?.graphContext?.eventCount + 1, - context: null, - realtimeData: null, - nodes: null, - params: null, - graph: null, - }; + space?.graphContextDestroyed$?.next( + GraphContextDestroyReasonMessage.RECEIVE_WILL_DESTROY_EVENT, + ); }, [WebAudioDebuggerEvent.nodeParamConnected]: ( @@ -517,61 +576,78 @@ Context is of type '${contextCreated.context.contextType}' but should be 'realti return context; }, + [PageDebuggerEvent.frameNavigated]: (helpers, contexts) => { + console.debug( + `Checking if tracked Audio Contexts (${Object.keys(contexts) + .map((contextId) => contextId.slice(-6)) + .join(', ')}) exist after frame navigated.`, + ); + + return ensureContextsExist(contexts, helpers); + }, + + [PageDebuggerEvent.loadEventFired]: (helpers, contexts) => { + console.debug( + `Checking if tracked Audio Contexts (${Object.keys(contexts) + .map((contextId) => contextId.slice(-6)) + .join(', ')}) exist after load event.`, + ); + + return ensureContextsExist(contexts, helpers); + }, + [ChromeDebuggerAPIEventName.detached]: ( helpers, contexts, debuggerDetached, ) => { if (debuggerDetached.reason === 'target_closed') { - return merge( - ...Object.keys(contexts).map((contextId) => - helpers.realtimeData.pollContext(contextId).pipe( - take(1), - ignoreElements(), - catchError((reason) => { - if (reason && reason.message && !reason.code) { - try { - reason = JSON.parse(reason.message); - } catch (e) {} - } - - if ( - reason && - reason.message === RealtimeDataErrorMessage.CANNOT_FIND - ) { - const space = contexts[contextId]; - if (space) { - delete contexts[contextId]; - space?.graphContextDestroyed$?.next(); - return of({ - id: contextId, - eventCount: space?.graphContext?.eventCount + 1, - context: null, - realtimeData: null, - nodes: null, - params: null, - graph: null, - } as Audion.GraphContext); - } - } else if ( - reason && - reason.message === RealtimeDataErrorMessage.REALTIME_ONLY - ) { - // OfflineAudioContexts emit this error if they are still alive. - } else { - console.error(`Unknown error determining if context '${contextId}' is stale with devtools protocol WebAudio.getRealtimeData. -"${reason && reason.message ? reason.message : reason}"`); - } - - return EMPTY; - }), - ), - ), + console.debug( + `Checking if tracked Audio Contexts (${Object.keys(contexts) + .map((contextId) => contextId.slice(-6)) + .join( + ', ', + )}) exist after debugger detached because target was closed.`, ); + + return ensureContextsExist(contexts, helpers); } }, }; +function ensureContextsExist( + contexts: MutableContexts, + helpers: EventHelpers, +): void | Audion.GraphContext | Observable { + return merge( + ...Object.keys(contexts).map((contextId) => + helpers.realtimeData.pollContext(contextId).pipe( + take(1), + ignoreElements(), + catchError((reason) => { + reason = WebAudioRealtimeDataReason.parseReason(reason); + + if (WebAudioRealtimeDataReason.isCannotFindReason(reason)) { + const space = contexts[contextId]; + if (space) { + space?.graphContextDestroyed$?.next( + GraphContextDestroyReasonMessage.CANNOT_FIND_REALTIME_DATA, + ); + } + } else if (WebAudioRealtimeDataReason.isRealtimeOnlyReason(reason)) { + // OfflineAudioContexts emit this error if they are still alive. + } else { + console.error(`Unexpected error determining if context '${contextId}' is stale with devtools protocol WebAudio.getRealtimeData. +"${WebAudioRealtimeDataReason.toString(reason)}"`); + } + + return EMPTY; + }), + ), + ), + ); +} + function removeAll(array: T[], fn: (value: T) => boolean) { if (array) { let index = array.findIndex(fn); diff --git a/src/devtools/WebAudioRealtimeData.ts b/src/devtools/WebAudioRealtimeData.ts index dc9dbf0..b54b5c2 100644 --- a/src/devtools/WebAudioRealtimeData.ts +++ b/src/devtools/WebAudioRealtimeData.ts @@ -1,6 +1,6 @@ import Protocol from 'devtools-protocol'; -import {bindCallback, concatMap, interval} from 'rxjs'; -import {map} from 'rxjs/operators'; +import {bindCallback, concatMap, interval, Observable} from 'rxjs'; +import {map, timeout} from 'rxjs/operators'; import {invariant} from '../utils/error'; @@ -8,6 +8,7 @@ import {chrome} from '../chrome'; import {WebAudioDebuggerMethod} from '../chrome/DebuggerWebAudioDomain'; import {Audion} from './Types'; +import {bindChromeCallback} from '../utils/rxChrome'; /** * Error messages returned by WebAudio.getRealtimeData devtool protocol method. @@ -19,12 +20,16 @@ export enum RealtimeDataErrorMessage { REALTIME_ONLY = 'ContextRealtimeData is only avaliable for an AudioContext.', } +interface RealtimeDataReason { + message: Message; +} + const {tabId} = chrome.devtools.inspectedWindow; -const sendCommand = bindCallback< +const sendCommand = bindChromeCallback< [{tabId: string}, WebAudioDebuggerMethod.getRealtimeData, any?], [{realtimeData: Protocol.WebAudio.ContextRealtimeData}] ->(chrome.debugger.sendCommand.bind(chrome.debugger)); +>(chrome.debugger.sendCommand, chrome.debugger); export const INITIAL_CONTEXT_REALTIME_DATA = { callbackIntervalMean: 0, @@ -35,6 +40,7 @@ export const INITIAL_CONTEXT_REALTIME_DATA = { export class WebAudioRealtimeData { private readonly intervalMS = 1000; + private readonly timeoutMS = 500; private readonly interval$ = interval(this.intervalMS); @@ -44,10 +50,8 @@ export class WebAudioRealtimeData { sendCommand({tabId}, WebAudioDebuggerMethod.getRealtimeData, { contextId, }).pipe( + timeout({first: this.timeoutMS}), map((result) => { - if (chrome.runtime.lastError) { - throw chrome.runtime.lastError; - } invariant( result && result !== null, 'ContextRealtimeData not returned for WebAudio context %0.', @@ -60,3 +64,30 @@ export class WebAudioRealtimeData { ); } } + +export const WebAudioRealtimeDataReason = { + parseReason(reason: any) { + if (reason && reason.message && !reason.code) { + try { + reason = JSON.parse(reason.message); + } catch (e) {} + } + return reason; + }, + + toString(reason: any) { + return reason && reason.message ? reason.message : reason; + }, + + isRealtimeOnlyReason( + reason: any, + ): reason is RealtimeDataReason { + return reason && reason.message === RealtimeDataErrorMessage.REALTIME_ONLY; + }, + + isCannotFindReason( + reason: any, + ): reason is RealtimeDataReason { + return reason && reason.message === RealtimeDataErrorMessage.CANNOT_FIND; + }, +}; diff --git a/src/devtools/main.ts b/src/devtools/main.ts index 7c96c81..1db03e1 100644 --- a/src/devtools/main.ts +++ b/src/devtools/main.ts @@ -14,17 +14,23 @@ import {Audion} from './Types'; import {DebuggerAttachEventController} from './DebuggerAttachEventController'; import {DevtoolsGraphPanel} from './DevtoolsGraphPanel'; import {serializeGraphContext} from './serializeGraphContext'; -import {WebAudioEventObservable} from './WebAudioEventObserver'; import {integrateWebAudioGraph} from './WebAudioGraphIntegrator'; import {WebAudioRealtimeData} from './WebAudioRealtimeData'; import {partitionMap} from './partitionMap'; +import {DebuggerEventsObservable} from './DebuggerEvents'; const attachController = new DebuggerAttachEventController(); -const webAudioEvents$ = new WebAudioEventObservable(attachController); +const pageEvent$ = new DebuggerEventsObservable(attachController, { + domain: 'page', +}); +const webAudioEvents$ = new DebuggerEventsObservable(attachController, { + domain: 'webAudio', +}); const webAudioRealtimeData = new WebAudioRealtimeData(); const serializedGraphContext$ = merge( + pageEvent$, webAudioEvents$, attachController.debuggerEvent$, ).pipe( diff --git a/src/panel.html b/src/panel.html index b89ce20..38ad2f3 100644 --- a/src/panel.html +++ b/src/panel.html @@ -408,6 +408,6 @@

Loading ...

- + diff --git a/src/panel/graph/AudioNodeBackgroundRenderCacheGroup.ts b/src/panel/graph/AudioNodeBackgroundRenderCacheGroup.ts index 09456df..06b752c 100644 --- a/src/panel/graph/AudioNodeBackgroundRenderCacheGroup.ts +++ b/src/panel/graph/AudioNodeBackgroundRenderCacheGroup.ts @@ -22,7 +22,6 @@ export class AudioNodeBackgroundCache { getBackground(node: Audion.GraphNode) { if (!this.cache.has(node.node.nodeType)) { - console.log(node); const background = new AudioNodeBackground(); background.init(AudioNodeMetrics.from(node, this.textCacheGroup)); this.cache.set(node.node.nodeType, background); diff --git a/src/panel/main.ts b/src/panel/main.ts index 05a9bd3..2016bb9 100644 --- a/src/panel/main.ts +++ b/src/panel/main.ts @@ -87,7 +87,7 @@ const graphContainer = const graphRender = new AudioGraphRender({elementContainer: graphContainer}); graphRender.init(); -const layoutWorker = new Worker('panelWorker.js'); +const layoutWorker = new Worker('audion-panelWorker.js'); graphSelector.graph$ .pipe( diff --git a/src/utils/rxChrome.ts b/src/utils/rxChrome.ts new file mode 100644 index 0000000..8dfb235 --- /dev/null +++ b/src/utils/rxChrome.ts @@ -0,0 +1,48 @@ +import {fromEventPattern, Observable} from 'rxjs'; +import {chrome} from '../chrome'; +import {ChromeDebuggerAPIEvent} from '../devtools/DebuggerAttachEventController'; + +/** + * Create a function that returns an observable that completes when the api + * calls back. + * @param method `chrome` api method whose last argument is a callback + * @param thisArg `this` inside of the method + * @returns observable that completes when the method is done + */ +export function bindChromeCallback

( + method: (...args: [...params: P, callback: (...values: R) => void]) => void, + thisArg = null, +) { + return (...args: P) => + new Observable( + (subscriber) => { + method.call(thisArg, ...args, (...returnValues: R) => { + if (chrome.runtime.lastError) { + subscriber.error(chrome.runtime.lastError); + } else { + if (returnValues.length === 0) { + subscriber.next(); + } else if (returnValues.length === 1) { + subscriber.next(returnValues[0]); + } else if (returnValues.length > 1) { + subscriber.next(returnValues as any); + } + subscriber.complete(); + } + }); + }, + ); +} + +export const fromChromeEvent = any>( + onEvent: Chrome.Event, +) => + fromEventPattern< + Parameters extends infer T1 + ? T1 extends [] + ? void + : T1 extends [infer T2] + ? T2 + : T1 + : never + >(onEvent.addListener.bind(onEvent), onEvent.removeListener.bind(onEvent)); diff --git a/src/webpack.config.js b/src/webpack.config.js index e079292..11b8c5e 100644 --- a/src/webpack.config.js +++ b/src/webpack.config.js @@ -3,9 +3,9 @@ const {resolve} = require('path'); module.exports = (env, argv) => ({ context: __dirname, entry: { - devtools: './devtools/main', - panel: './panel/main', - panelWorker: './panel/worker', + 'audion-devtools': './devtools/main', + 'audion-panel': './panel/main', + 'audion-panelWorker': './panel/worker', }, output: { path: resolve(__dirname, '../build/audion'),