Skip to content

Commit 699a7d6

Browse files
committed
fix: implement replay of css events in the proxy
1 parent 0ced734 commit 699a7d6

File tree

3 files changed

+136
-10
lines changed

3 files changed

+136
-10
lines changed

src/adapter/cdpProxy.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,40 @@ describe('CdpProxyProvider', () => {
128128
await client.session.sendOrDie('Runtime.evaluate', { expression: '' });
129129
expect(recv).to.deep.equal(['Runtime.consoleAPICalled', 'Debugger.scriptParsed']);
130130
});
131+
132+
describe('replays', () => {
133+
it('CSS', async () => {
134+
transport.onDidSendEmitter.event(async message => {
135+
await delay(0);
136+
transport.injectMessage({
137+
id: message.id as number,
138+
result: {},
139+
sessionId: message.sessionId,
140+
});
141+
});
142+
143+
transport.injectMessage({
144+
method: 'CSS.styleSheetAdded',
145+
params: { styleSheetId: '42' },
146+
sessionId: 'sesh',
147+
});
148+
transport.injectMessage({
149+
method: 'CSS.styleSheetAdded',
150+
params: { styleSheetId: '43' },
151+
sessionId: 'sesh',
152+
});
153+
transport.injectMessage({
154+
method: 'CSS.styleSheetRemoved',
155+
params: { styleSheetId: '43' },
156+
sessionId: 'sesh',
157+
});
158+
159+
const events: unknown[] = [];
160+
client.CSS.on('styleSheetAdded', evt => events.push(evt));
161+
client.CSS.on('styleSheetRemoved', evt => events.push(evt));
162+
163+
expect(await client.CSS.enable({})).to.deep.equal({});
164+
expect(events).to.deep.equal([{ styleSheetId: '42' }]);
165+
});
166+
});
131167
});

src/adapter/cdpProxy.ts

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import Dap from '../dap/api';
1414
import { acquireTrackedWebSocketServer, IPortLeaseTracker } from './portLeaseTracker';
1515

1616
const jsDebugDomain = 'JsDebug';
17-
const jsDebugMethodPrefix = jsDebugDomain + '.';
1817
const eventWildcard = '*';
1918

2019
/**
@@ -55,6 +54,64 @@ export interface ICdpProxyProvider extends IDisposable {
5554
proxy(): Promise<Dap.RequestCDPProxyResult>;
5655
}
5756

57+
type ReplayMethod = { event: string; params: Record<string, unknown> };
58+
59+
/**
60+
* Handles replaying events from domains. Certain events are only fired when
61+
* a domain is first enabled, so subsequent connections may not receive it.
62+
*/
63+
class DomainReplays {
64+
private replays = new Map<keyof Cdp.Api, ReplayMethod[]>();
65+
66+
/**
67+
* Adds a message to be replayed.
68+
*/
69+
public addReplay(domain: keyof Cdp.Api, event: string, params: unknown) {
70+
const obj = { event: `${domain}.${event}`, params: params as Record<string, unknown> };
71+
const arr = this.replays.get(domain);
72+
if (arr) {
73+
arr.push(obj);
74+
} else {
75+
this.replays.set(domain, [obj]);
76+
}
77+
}
78+
79+
/**
80+
* Captures replay for the event on CDP.
81+
*/
82+
public capture(cdp: Cdp.Api, domain: keyof Cdp.Api, event: string) {
83+
(cdp[domain] as {
84+
on(event: string, fn: (arg: Record<string, unknown>) => void): void;
85+
}).on(event, evt => this.addReplay(domain, event, evt));
86+
}
87+
88+
/**
89+
* Filters replayed events.
90+
*/
91+
public filterReply(domain: keyof Cdp.Api, filterFn: (r: ReplayMethod) => boolean) {
92+
const arr = this.replays.get(domain);
93+
if (!arr) {
94+
return;
95+
}
96+
97+
this.replays.set(domain, arr.filter(filterFn));
98+
}
99+
100+
/**
101+
* Removes all replay info for a domain.
102+
*/
103+
public clear(domain: keyof Cdp.Api) {
104+
this.replays.delete(domain);
105+
}
106+
107+
/**
108+
* Gets replay messages for the given domain.
109+
*/
110+
public read(domain: keyof Cdp.Api) {
111+
return this.replays.get(domain) ?? [];
112+
}
113+
}
114+
58115
export const ICdpProxyProvider = Symbol('ICdpProxyProvider');
59116

60117
/**
@@ -64,6 +121,7 @@ export const ICdpProxyProvider = Symbol('ICdpProxyProvider');
64121
export class CdpProxyProvider implements ICdpProxyProvider {
65122
private server?: Promise<{ server: WebSocket.Server; path: string }>;
66123
private readonly disposables = new DisposableList();
124+
private readonly replay = new DomainReplays();
67125

68126
private jsDebugApi: IJsDebugDomain = {
69127
/** @inheritdoc */
@@ -90,7 +148,20 @@ export class CdpProxyProvider implements ICdpProxyProvider {
90148
@inject(ICdpApi) private readonly cdp: Cdp.Api,
91149
@inject(IPortLeaseTracker) private readonly portTracker: IPortLeaseTracker,
92150
@inject(ILogger) private readonly logger: ILogger,
93-
) {}
151+
) {
152+
this.replay.capture(cdp, 'CSS', 'fontsUpdated');
153+
this.replay.capture(cdp, 'CSS', 'styleSheetAdded');
154+
155+
cdp.CSS.on('fontsUpdated', evt => {
156+
if (evt.font) {
157+
this.replay.addReplay('CSS', 'fontsUpdated', evt);
158+
}
159+
});
160+
161+
cdp.CSS.on('styleSheetRemoved', evt =>
162+
this.replay.filterReply('CSS', s => s.params.styleSheetId !== evt.styleSheetId),
163+
);
164+
}
94165

95166
/**
96167
* Acquires the proxy server, and returns its address.
@@ -139,14 +210,12 @@ export class CdpProxyProvider implements ICdpProxyProvider {
139210
this.logger.verbose(LogTag.ProxyActivity, 'received proxy message', message);
140211

141212
const { method, params, id = 0 } = message;
213+
const [domain, fn] = method.split('.');
142214
try {
143-
const result = method.startsWith(jsDebugMethodPrefix)
144-
? await this.invokeJsDebugDomainMethod(
145-
clientHandle,
146-
method.slice(jsDebugMethodPrefix.length),
147-
params,
148-
)
149-
: await this.cdp.session.sendOrDie(method, params);
215+
const result =
216+
domain === jsDebugDomain
217+
? await this.invokeJsDebugDomainMethod(clientHandle, fn, params)
218+
: await this.invokeCdpMethod(clientHandle, domain, fn, params);
150219
clientHandle.send({ id, result });
151220
} catch (e) {
152221
const error =
@@ -168,11 +237,31 @@ export class CdpProxyProvider implements ICdpProxyProvider {
168237
this.server = undefined;
169238
}
170239

240+
private invokeCdpMethod(client: ClientHandle, domain: string, method: string, params: object) {
241+
const promise = this.cdp.session.sendOrDie(`${domain}.${method}`, params);
242+
switch (method) {
243+
case 'enable':
244+
this.replay
245+
.read(domain as keyof Cdp.Api)
246+
.forEach(m => client.send({ method: m.event, params: m.params }));
247+
break;
248+
case 'disable':
249+
this.replay.clear(domain as keyof Cdp.Api);
250+
break;
251+
default:
252+
// no-op
253+
}
254+
255+
// it's intentional that replay is sent before the
256+
// enabled response; this is what Chrome does.
257+
return promise;
258+
}
259+
171260
private invokeJsDebugDomainMethod(handle: ClientHandle, method: string, params: unknown) {
172261
if (!this.jsDebugApi.hasOwnProperty(method)) {
173262
throw new ProtocolError(method).setCause(
174263
ProxyErrors.MethodNotFound,
175-
`${jsDebugMethodPrefix}${method} not found`,
264+
`${jsDebugDomain}.${method} not found`,
176265
);
177266
}
178267

src/build/generate-contributions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,7 @@ const commands: ReadonlyArray<{
12531253
command: Commands.OpenEdgeDevTools,
12541254
title: refString('openEdgeDevTools.label'),
12551255
icon: '$(inspect)',
1256+
category: 'Debug',
12561257
},
12571258
];
12581259

0 commit comments

Comments
 (0)