Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(tsc): add type checking to use of CRDP events #4886

Merged
merged 3 commits into from
Mar 30, 2018
Merged

Conversation

brendankenny
Copy link
Member

@brendankenny brendankenny commented Mar 28, 2018

OK! Took a bit longer than I hoped, but working now and hopefully a maintainable solution because it adds significant usability improvements.

Adds type checking and type inference to Chrome Remote Debugging Protocol events listened to via Driver:

events-animated

The emitter type is based on https://github.com/bterlson/strict-event-emitter-types/, but it turns out we can't use it as-is in JS because typescript doesn't check @implements yet, you can't do method overloading in JS, and overloading in a typescript d.ts file isn't compatible with union types (which we can use in JS).

A breakdown of this PR:

  1. third-party/strict-event-emitter-types, LICENSE and index.d.ts.
    This is a forked version of strict-event-emitter-types replacing method overloads with type unions. The changes reduce the flexibility of the emit interface, so unfortunately I don't think we'll be able to upstream these changes (at least until future TS changes), but the loss of flexibility turns out to have basically no effect on us since our events are coming over the wire anyways.

  2. scripts/extract-crdp-mapping.js and typings/crdp-mapping.d.ts:
    extract-crdp-mapping.js walks the protocol d.ts file using the typescript compiler API and pulls out an interface with keys that are all the event names (Console.messageAdded, Debugger.breakpointResolved, etc) mapped to the expected event payload type (LH.Crdp.Console.MessageAddedEvent, LH.Crdp.Debugger.BreakpointResolvedEvent, etc).

    extract-crdp-mapping.js can be run with yarn update:crdp-typings and is saved to typings/crdp-mapping.d.ts so it can be used for typing. The event map is used to tell StrictEventEmitter what event payload type is expected to be received (or emitted) based on the event string.

    When protocol-dts-generator for devtools-protocol typescript types ChromeDevTools/devtools-protocol#90 lands we can hopefully get rid of both of these files and upstream the interface into the official type declaration as Crdp.AllEvents or whatever.

  3. typings/protocol.d.ts:
    Creates some union types out of the event map in crdp-mapping.d.ts that are compatible with a StrictEventEmitter that's created from the same event map.

  4. all the rest (connection and driver, mostly)
    Altered to use these libraries. The main pain point is that typescript doesn't support constrained templates in JSDocs right now, so there's no way to express the constraints we want to on Driver.on, Connection.on, etc using the normal @param @return jsdocs. However the body of @type does support most of what we want to do, and while you can't add @type to a method definition in a Class body, you can add them to a function expression assigned to the class prototype outside that body :)

    So the on/once/off methods have been pulled out (unaltered) and defined at the bottom of the files, and then we can conveniently just type them as the event emitter method (e.g. CrdpEventEmitter['on']) and it's not too messy. Any other changes are just to pipe those specific types through.

The PR is a bit long, but as noted, hopefully extract-crdp-mapping.js and crdp-mapping.d.ts can be deleted in the near future.

@brendankenny brendankenny changed the title core(tsc): add type checking to use of CRDP events core(tsc): add type checking to use of CRDP events Mar 28, 2018
@wardpeet
Copy link
Collaborator

#excited

if (!this._eventEmitter) {
throw new Error('Attempted to emit event after connection disposed.');
}
this._eventEmitter.emit('notification', {method, params});

this._eventEmitter.emit('protocolevent', eventMessage);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed this with @paulirish earlier. Since this event emitter can only emit protocol events (and Driver is the only receiver), it makes sense to make the single event it handles be something more descriptive than "notification" :)

@@ -946,8 +911,8 @@ class Driver {
endTrace() {
return new Promise((resolve, reject) => {
// When the tracing has ended this will fire with a stream handle.
this.once('Tracing.tracingComplete', streamHandle => {
this._readTraceFromStream(streamHandle)
this.once('Tracing.tracingComplete', completeEvent => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that these are now type checked (not any!) but they are also inferred correctly from the event string, so no annotation needed :)

@@ -83,7 +84,7 @@
"postinstall-prepare": "^1.0.1",
"puppeteer": "^1.1.1",
"sinon": "^2.3.5",
"typescript": "^2.8.0-rc",
"typescript": "2.9.0-dev.20180323",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typescript 2.8.1 just came out with all the typescript features we need, but unfortunately the PR that extends them to javascript didn't get pulled over to the 2.8 branch and now won't be seen until 2.9 (microsoft/TypeScript#22692 (comment)). I did some testing and this is the closest nightly to the 2.8.1 release that has the commit we need and is stable, so I went with this. They're on a 6-8 week cadence, so hopefully we can switch to 2.9 in May-ish

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mostly glazed over trying to read bterlson's d.ts again 😆 but js changes seem pretty minimal, awesome job! 💯

const path = require('path');
const ts = require('typescript');

const crdpTypingFile = './node_modules/vscode-chrome-debug-core/lib/crdp/crdp.d.ts';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we require resolve this one?

export import Crdp = _Crdp;
export type StrictEventEmitter<TEmitterType, TEventRecord, TEmitRecord = TEventRecord> =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never really got why we need to keep the EventEmitter component, while we're aliasing shall we just flip and default it to EventEmitter? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh, good idea

[K in keyof LH.CrdpEvents]: {
method: K,
// Drop void for `undefined` (so a JS value is valid). See above TODO
params: LH.CrdpEvents[K] extends void ? undefined: LH.CrdpEvents[K]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so here's the magic line, huh. incredible 💘 that tsc

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM let's kick the 🚗 on this 💯

@brendankenny
Copy link
Member Author

@paulirish also gave the 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants