change: add debugger state machine to WebAudioEventObserver
This commit is contained in:
Родитель
adc7dad243
Коммит
4399ae9e2d
|
@ -52,7 +52,7 @@ function noopChrome() {
|
|||
getURL(url) {
|
||||
return url;
|
||||
},
|
||||
lastError: {message: ''},
|
||||
lastError: undefined,
|
||||
onConnect: noopEvent(),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import {beforeEach, describe, expect, it, jest} from '@jest/globals';
|
||||
|
||||
import {chrome} from '../chrome';
|
||||
import {Events} from '../chrome/DebuggerWebAudioDomain';
|
||||
import {Event} from '../chrome/DebuggerWebAudioDomain';
|
||||
|
||||
import {WebAudioEventObserver} from './WebAudioEventObserver';
|
||||
|
||||
|
@ -53,12 +53,19 @@ describe('WebAudioEventObserver', () => {
|
|||
if (jest.isMockFunction(chrome.debugger.attach)) {
|
||||
/** @type {function} */ (chrome.debugger.attach.mock.calls[0][2])();
|
||||
}
|
||||
expect(chrome.debugger.sendCommand).toBeCalledTimes(1);
|
||||
if (jest.isMockFunction(chrome.debugger.sendCommand)) {
|
||||
/** @type {function} */ (chrome.debugger.sendCommand.mock.calls[0][2])();
|
||||
}
|
||||
unsubscribe();
|
||||
expect(chrome.debugger.detach).toBeCalled();
|
||||
if (jest.isMockFunction(chrome.debugger.sendCommand)) {
|
||||
/** @type {function} */ (chrome.debugger.sendCommand.mock.calls[1][2])();
|
||||
}
|
||||
expect(chrome.debugger.sendCommand).toBeCalledTimes(2);
|
||||
if (jest.isMockFunction(chrome.debugger.detach)) {
|
||||
/** @type {function} */ (chrome.debugger.detach.mock.calls[0][1])();
|
||||
}
|
||||
expect(chrome.debugger.sendCommand).toBeCalledTimes(2);
|
||||
expect(chrome.debugger.onDetach.removeListener).toBeCalled();
|
||||
expect(chrome.debugger.onEvent.removeListener).toBeCalled();
|
||||
});
|
||||
|
@ -85,10 +92,10 @@ describe('WebAudioEventObserver', () => {
|
|||
if (jest.isMockFunction(chrome.debugger.onEvent.addListener)) {
|
||||
/** @type {function} */ (
|
||||
chrome.debugger.onEvent.addListener.mock.calls[0][0]
|
||||
)('tab', Events.contextCreated, contextCreated);
|
||||
)('tab', Event.contextCreated, contextCreated);
|
||||
}
|
||||
expect(nextMock).toBeCalledWith({
|
||||
method: Events.contextCreated,
|
||||
method: Event.contextCreated,
|
||||
params: contextCreated,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,21 @@
|
|||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
concat,
|
||||
defer,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
ObservedValueOf,
|
||||
of,
|
||||
scan,
|
||||
share,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
|
||||
import {chrome} from '../chrome';
|
||||
import {Methods} from '../chrome/DebuggerWebAudioDomain';
|
||||
import {Method} from '../chrome/DebuggerWebAudioDomain';
|
||||
import {Observer} from '../utils/Observer';
|
||||
import {Audion} from './Types';
|
||||
|
||||
|
@ -11,6 +27,10 @@ const {tabId} = chrome.devtools.inspectedWindow;
|
|||
* @alias WebAudioEventObserver
|
||||
*/
|
||||
export class WebAudioEventObserver extends Observer<Audion.WebAudioEvent> {
|
||||
permission: PermissionSubject;
|
||||
attachInterest: CounterSubject;
|
||||
eventInterest: CounterSubject;
|
||||
|
||||
/**
|
||||
* Observe WebAudio events from chrome.debugger.
|
||||
*/
|
||||
|
@ -33,19 +53,295 @@ export class WebAudioEventObserver extends Observer<Audion.WebAudioEvent> {
|
|||
chrome.debugger.onDetach.addListener(onDetach);
|
||||
chrome.debugger.onEvent.addListener(onEvent);
|
||||
|
||||
this.attachInterest.increment();
|
||||
this.eventInterest.increment();
|
||||
|
||||
return () => {
|
||||
chrome.debugger.onDetach.removeListener(onDetach);
|
||||
chrome.debugger.onEvent.removeListener(onEvent);
|
||||
chrome.debugger.sendCommand({tabId}, Methods.disable);
|
||||
chrome.debugger.detach({tabId}, () => {});
|
||||
|
||||
this.eventInterest.decrement();
|
||||
this.attachInterest.decrement();
|
||||
};
|
||||
});
|
||||
|
||||
const permission = (this.permission = new PermissionSubject());
|
||||
|
||||
const attachInterest = (this.attachInterest = new CounterSubject(0));
|
||||
|
||||
const attachSubject = new AttachStateSubject();
|
||||
|
||||
const eventInterest = (this.eventInterest = new CounterSubject(0));
|
||||
|
||||
const eventSubject = new EventStateSubject();
|
||||
|
||||
const debuggerState = merge(
|
||||
permission.pipe(map((permission) => ({permission}))),
|
||||
attachInterest.pipe(map((attachInterest) => ({attachInterest}))),
|
||||
attachSubject.pipe(map((attachState) => ({attachState}))),
|
||||
eventInterest.pipe(map((eventInterest) => ({eventInterest}))),
|
||||
eventSubject.pipe(map((eventState) => ({eventState}))),
|
||||
).pipe(
|
||||
scan((accum, value) => ({...accum, ...value}), {
|
||||
permission: AttachPermission.UNKNOWN,
|
||||
attachInterest: 0,
|
||||
attachState: AttachState.DETACHED,
|
||||
eventInterest: 0,
|
||||
eventState: EventState.DISABLED,
|
||||
}),
|
||||
distinctUntilChanged(
|
||||
(previous, current) =>
|
||||
previous.permission === current.permission &&
|
||||
previous.attachInterest === current.attachInterest &&
|
||||
previous.attachState === current.attachState &&
|
||||
previous.eventInterest === current.eventInterest &&
|
||||
previous.eventState === current.eventState,
|
||||
),
|
||||
share(),
|
||||
);
|
||||
|
||||
debuggerState.subscribe({
|
||||
next: (state) => {
|
||||
if (
|
||||
state.permission === AttachPermission.TEMPORARY &&
|
||||
state.attachInterest > 0
|
||||
) {
|
||||
attachSubject.attach();
|
||||
} else {
|
||||
attachSubject.detach();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
onDebuggerDetach.subscribe({
|
||||
next([, reason]) {
|
||||
if (reason === 'canceled_by_user') {
|
||||
permission.reject();
|
||||
}
|
||||
attachSubject.next(AttachState.DETACHED);
|
||||
},
|
||||
});
|
||||
|
||||
debuggerState.subscribe({
|
||||
next(state) {
|
||||
if (
|
||||
state.attachState === AttachState.ATTACHED &&
|
||||
state.eventInterest > 0
|
||||
) {
|
||||
eventSubject.enable();
|
||||
} else {
|
||||
if (state.attachState === AttachState.ATTACHED) {
|
||||
eventSubject.disable();
|
||||
} else {
|
||||
eventSubject.next(EventState.DISABLED);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Attaches the chrome.debugger to start observing events. */
|
||||
attach() {
|
||||
chrome.debugger.attach({tabId}, debuggerVersion, () => {
|
||||
chrome.debugger.sendCommand({tabId}, Methods.enable);
|
||||
});
|
||||
this.permission.grantTemporary();
|
||||
}
|
||||
}
|
||||
|
||||
enum AttachState {
|
||||
DETACHING = 'detaching',
|
||||
DETACHED = 'detached',
|
||||
ATTACHING = 'attaching',
|
||||
ATTACHED = 'attached',
|
||||
}
|
||||
|
||||
function bindChromeCallback<P extends any[]>(
|
||||
method: (...args: [...params: P, callback: () => void]) => void,
|
||||
thisArg = null,
|
||||
) {
|
||||
return (...args: P) =>
|
||||
new Observable<void>((subscriber) => {
|
||||
method.call(thisArg, ...args, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
subscriber.error(chrome.runtime.lastError);
|
||||
} else {
|
||||
subscriber.complete();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function fromChromeEvent<A extends any[]>(
|
||||
event: Chrome.Event<(...args: A) => any>,
|
||||
) {
|
||||
return new Observable<A>((subscriber) => {
|
||||
const listener = (...args: A) => {
|
||||
subscriber.next(args);
|
||||
};
|
||||
event.addListener(listener);
|
||||
return () => {
|
||||
event.removeListener(listener);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const attach = bindChromeCallback(chrome.debugger.attach, chrome.debugger);
|
||||
const detach = bindChromeCallback(chrome.debugger.detach, chrome.debugger);
|
||||
const sendCommand = bindChromeCallback(
|
||||
chrome.debugger.sendCommand,
|
||||
chrome.debugger,
|
||||
);
|
||||
|
||||
const onDebuggerEvent = fromChromeEvent<
|
||||
[target: Chrome.DebuggerDebuggee, method: Method, params: any]
|
||||
>(chrome.debugger.onEvent);
|
||||
const onDebuggerDetach = fromChromeEvent<
|
||||
[target: Chrome.DebuggerDebuggee, reason: string]
|
||||
>(chrome.debugger.onDetach);
|
||||
|
||||
enum AttachPermission {
|
||||
UNKNOWN,
|
||||
TEMPORARY,
|
||||
REJECTED,
|
||||
}
|
||||
|
||||
enum EventState {
|
||||
DISABLING,
|
||||
DISABLED,
|
||||
ENABLING,
|
||||
ENABLED,
|
||||
}
|
||||
|
||||
class PermissionSubject extends BehaviorSubject<AttachPermission> {
|
||||
constructor() {
|
||||
super(AttachPermission.UNKNOWN);
|
||||
}
|
||||
|
||||
grantTemporary() {
|
||||
if (this.value === AttachPermission.UNKNOWN) {
|
||||
this.next(AttachPermission.TEMPORARY);
|
||||
}
|
||||
}
|
||||
|
||||
reject() {
|
||||
if (this.value !== AttachPermission.REJECTED) {
|
||||
this.next(AttachPermission.REJECTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TransitionSubject<T> extends BehaviorSubject<T> {
|
||||
transition(options: {
|
||||
expectState: T;
|
||||
transitionState: T;
|
||||
finalState: T;
|
||||
errorState?: T;
|
||||
transit: () => Observable<void>;
|
||||
}) {
|
||||
if (this.value === options.expectState) {
|
||||
concat(
|
||||
of(options.transitionState),
|
||||
options.transit(),
|
||||
defer(() =>
|
||||
this.value === options.transitionState
|
||||
? of(options.finalState)
|
||||
: of(),
|
||||
),
|
||||
)
|
||||
.pipe(
|
||||
catchError((err) =>
|
||||
of(
|
||||
this.value === options.transitionState
|
||||
? options.errorState || options.expectState
|
||||
: options.expectState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe({next: this.next.bind(this)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AttachStateSubject extends TransitionSubject<AttachState> {
|
||||
private readonly attachTransition = {
|
||||
expectState: AttachState.DETACHED,
|
||||
transitionState: AttachState.ATTACHING,
|
||||
transit: () => attach({tabId}, debuggerVersion),
|
||||
finalState: AttachState.ATTACHED,
|
||||
};
|
||||
|
||||
private readonly detachTransition = {
|
||||
expectState: AttachState.ATTACHED,
|
||||
transitionState: AttachState.DETACHING,
|
||||
finalState: AttachState.DETACHED,
|
||||
transit: () => detach({tabId}),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super(AttachState.DETACHED);
|
||||
}
|
||||
|
||||
attach() {
|
||||
this.transition(this.attachTransition);
|
||||
}
|
||||
|
||||
detach() {
|
||||
this.transition(this.detachTransition);
|
||||
}
|
||||
}
|
||||
|
||||
class EventStateSubject extends TransitionSubject<EventState> {
|
||||
private readonly enableTransition = {
|
||||
expectState: EventState.DISABLED,
|
||||
transitionState: EventState.ENABLING,
|
||||
finalState: EventState.ENABLED,
|
||||
transit: () => sendCommand({tabId}, Method.enable),
|
||||
};
|
||||
|
||||
private readonly disableTransition = {
|
||||
expectState: EventState.ENABLED,
|
||||
transitionState: EventState.DISABLING,
|
||||
finalState: EventState.DISABLED,
|
||||
errorState: EventState.DISABLED,
|
||||
transit: () => sendCommand({tabId}, Method.disable),
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super(EventState.DISABLED);
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.transition(this.enableTransition);
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.transition(this.disableTransition);
|
||||
}
|
||||
}
|
||||
|
||||
class CounterSubject extends BehaviorSubject<number> {
|
||||
increment() {
|
||||
this.next(this.value + 1);
|
||||
}
|
||||
|
||||
decrement() {
|
||||
this.next(this.value - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function props<
|
||||
T extends {[key: string]: Observable<any>},
|
||||
R extends {[key in keyof T]: ObservedValueOf<T[key]>},
|
||||
>(
|
||||
source: T,
|
||||
initialValue: R = (Object.keys(source) as (keyof T)[]).reduce(
|
||||
(accum, key) => {
|
||||
accum[key] = undefined;
|
||||
return accum;
|
||||
},
|
||||
{} as R,
|
||||
),
|
||||
): Observable<R> {
|
||||
return merge(
|
||||
...Object.entries(source).map(([key, property]) =>
|
||||
property.pipe(map((value) => ({[key]: value}))),
|
||||
),
|
||||
).pipe(scan((accum, value) => ({...accum, ...value}), initialValue));
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче