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

[json-rpc-engine] Fix any types in catch blocks, refactor #3906

Merged
merged 20 commits into from
Mar 20, 2024

Conversation

MajorLift
Copy link
Contributor

@MajorLift MajorLift commented Feb 7, 2024

Explanation

  • Fixes all any usage in the JsonRpcEngine class.
  • Errors are typed consistently as unknown to reflect actual usage in consumers and tests. - JsonRpcEngineCallbackError is no longer used within the package.

References

Changelog

@metamask/json-rpc-engine

  • BREAKING: The type of the error parameters in JsonRpcEngineReturnHandler, JsonRpcEngineNextCallback, and JsonRpcEngineEndCallback are widened from JsonRpcEngineCallbackError to unknown.

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've highlighted breaking changes using the "BREAKING" category above as appropriate

@MajorLift MajorLift self-assigned this Feb 7, 2024
@MajorLift MajorLift force-pushed the 230205-fix-any-json-rpc-engine branch from 70119ee to c59d1b1 Compare February 7, 2024 21:33
@MajorLift MajorLift force-pushed the 230205-fix-any-json-rpc-engine branch 3 times, most recently from ca41d02 to 58949ad Compare March 7, 2024 03:44
@MajorLift MajorLift marked this pull request as ready for review March 7, 2024 03:51
@MajorLift MajorLift requested a review from a team as a code owner March 7, 2024 03:51
@MajorLift MajorLift force-pushed the 230205-fix-any-json-rpc-engine branch from 28b21db to c81e0cc Compare March 13, 2024 01:21
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

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

I left some minor comments but overall this looks good. Would be great to address these type issues as they seem to be multiplying, but that seems like another can of worms 😅

packages/json-rpc-engine/src/JsonRpcEngine.ts Outdated Show resolved Hide resolved
packages/json-rpc-engine/src/createAsyncMiddleware.ts Outdated Show resolved Hide resolved
return handlerCallback(error);
} catch (error) {
// TODO: Explicitly handle errors thrown from `#runReturnHandlers` that are not of type `JsonRpcEngineCallbackError`
return handlerCallback(error as JsonRpcEngineCallbackError);
Copy link
Contributor

@mcmire mcmire Mar 14, 2024

Choose a reason for hiding this comment

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

Out of interest, do we need to change the type of the error argument? That seems to necessitate adding a type assertion here, so we'd now be lying about the type of this error. I just want to make sure: do we think that this is preferable to using any?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On second glance, the error is being thrown by #runReturnHandlers, which is guaranteed to throw SerializableJsonRpcError | Error. Removed the assertion and added a comment explaining why: 4b61631

Copy link
Contributor Author

@MajorLift MajorLift Mar 15, 2024

Choose a reason for hiding this comment

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

All of the other type assertions with TODOs are end() calls.

With these, I think it doesn't make sense to inaccurately force error into the JsonRpcEngineCallbackError type when the test specs are passing in all error objects thrown by middleware into end callbacks regardless of their type.

IMO whatever type safety gains we get from narrowing the error type from unknown are cancelled out by the fact that we have to use inaccurate type assertions everywhere to make the narrower type constraint work.

I widened JsonRpcEngineCallback's error param to unknown, which lets us remove the TODO comments and the type assertions. What do you think? Would this be a regression in terms of type safety or too disruptive to downstream code?
bac8e0e b9549ed

Copy link
Contributor

Choose a reason for hiding this comment

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

I think these changes look better overall and make more sense than the previous changes.

packages/json-rpc-engine/src/JsonRpcEngine.ts Outdated Show resolved Hide resolved
packages/json-rpc-engine/src/createAsyncMiddleware.ts Outdated Show resolved Hide resolved
packages/json-rpc-engine/jest.config.js Outdated Show resolved Hide resolved
@MajorLift MajorLift force-pushed the 230205-fix-any-json-rpc-engine branch from 1df21c3 to ed282d8 Compare March 19, 2024 17:41
…own` and remove now unnecessary narrowing branches
Copy link
Contributor Author

@MajorLift MajorLift left a comment

Choose a reason for hiding this comment

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

I attempted a new approach of replacing all JsonRpcEngineCallbackError with unknown.

This cuts down on the diffs, makes a lot of the types consistent with how they're used in the tests, makes a lot of assertions unnecessary, and brings coverage back up to 100%.

I think this might be an appropriate scope for the current PR's objective (fix any). Implementing consistent usage of JsonRpcEngineCallbackError for the middleware callbacks should probably be handled in its own initiative.


export type JsonRpcEngineCallbackError = Error | SerializedJsonRpcError | null;

export type JsonRpcEngineReturnHandler = (
done: (error?: JsonRpcEngineCallbackError) => void,
done: (error?: unknown) => void,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

New strategy: consistently type all errors as unknown.

export type JsonRpcEngineEndCallback = (
error?: JsonRpcEngineCallbackError,
) => void;
export type JsonRpcEngineEndCallback = (error?: unknown) => void;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

New strategy: consistently type all errors as unknown.

hasProperty,
isJsonRpcNotification,
isJsonRpcRequest,
} from '@metamask/utils';

export type JsonRpcEngineCallbackError = Error | SerializedJsonRpcError | null;
Copy link
Contributor Author

@MajorLift MajorLift Mar 20, 2024

Choose a reason for hiding this comment

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

JsonRpcEngineCallbackError is no longer used anywhere in core or extension. Mobile has two instances in RPCMethodMiddleware.ts.

@@ -18,7 +21,7 @@ export type AsyncJsonrpcMiddleware<
next: AsyncJsonRpcEngineNextCallback,
) => Promise<void>;

type ReturnHandlerCallback = (error: null | Error) => void;
type ReturnHandlerCallback = Parameters<JsonRpcEngineReturnHandler>[0];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This definition is responsive to changes in JsonRpcEngineReturnHandler.

@@ -317,8 +328,8 @@ export class JsonRpcEngine extends SafeEventEmitter {
)
).filter(
// Filter out any notification responses.
(response) => response !== undefined,
) as JsonRpcResponse<Json>[];
(response): response is JsonRpcResponse<Json> => response !== undefined,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice! Good example usage of this.

| (JsonRpcRequest | JsonRpcNotification)[]
| JsonRpcRequest
| JsonRpcNotification,
callback?: (error: unknown, response: never) => void,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

never is used to widen the implementation signature enough to be compatible with all of the overload signatures.

Comment on lines +423 to +424
isJsonRpcNotification(callerReq) &&
!isJsonRpcRequest(callerReq)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

isJsonRpcNotification alone doesn't narrow callerReq to JsonRpcNotification.

Comment on lines +222 to +227
// This assertion is safe because of the runtime checks validating that `req` is an array and `callback` is defined.
// There is only one overload signature that satisfies both conditions, and its `callback` type is the one that's being asserted.
callback as (
error: unknown,
responses?: JsonRpcResponse<Json>[],
) => void,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally, TypeScript would have better pattern-matching around overload signatures and would be capable of narrowing callback and responses so that this assertion is unnecessary.

return this.#handle(req as JsonRpcRequest, callback);
return this.#handle(
req,
callback as (error: unknown, response?: JsonRpcResponse<Json>) => void,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The first and second overload signatures of handle are the only cases where req is a non-array and callback is defined. This is assertion is safe for those overload signatures.

Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

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

I just had one more comment, but this otherwise looks good to me.

Comment on lines 556 to 561
const parsedError = error || response.error;
if (parsedError) {
response.error = serializeError(parsedError);
}
// True indicates that the request should end
resolve([parsedError, true]);
resolve([parsedError ?? null, true]);
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that we are converting undefined to null here. This could matter, so what do you think about this:

Suggested change
const parsedError = error || response.error;
if (parsedError) {
response.error = serializeError(parsedError);
}
// True indicates that the request should end
resolve([parsedError, true]);
resolve([parsedError ?? null, true]);
const parsedError: unknown = error || response.error;
if (parsedError) {
response.error = serializeError(parsedError);
}
// True indicates that the request should end
resolve([parsedError, true]);

Copy link
Contributor Author

@MajorLift MajorLift Mar 20, 2024

Choose a reason for hiding this comment

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

Thanks for catching this. It's no longer necessary now that JsonRpcEngineCallbackError is not being used here.

Applied here: cfaa18b, except I didn't add the : unknown annotation because {} is NonNullable<unknown>, meaning unknown would pollute parsedError's type signature with a null that previously wasn't there.

Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

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

Everything looks good to me now. Nice work!

@MajorLift MajorLift merged commit 0c78d6d into main Mar 20, 2024
139 checks passed
@MajorLift MajorLift deleted the 230205-fix-any-json-rpc-engine branch March 20, 2024 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

json-rpc-engine: Replace use of any with proper types (non-test files only)
2 participants