Skip to content

refactor(@angular/cli): create a memoize decorator #22881

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

Merged
merged 1 commit into from
Apr 1, 2022
Merged

refactor(@angular/cli): create a memoize decorator #22881

merged 1 commit into from
Apr 1, 2022

Conversation

alan-agius4
Copy link
Collaborator

With this change we clean up repeated caching code by creating a memoize decorator that can be used on get accessors and methods.

@alan-agius4 alan-agius4 requested a review from clydin March 23, 2022 10:32
@alan-agius4 alan-agius4 added action: review The PR is still awaiting reviews from at least one requested reviewer target: major This PR is targeted for the next major release labels Mar 23, 2022
@alan-agius4 alan-agius4 marked this pull request as ready for review March 23, 2022 10:32
@alan-agius4 alan-agius4 requested review from dgp1130 March 30, 2022 05:38
@alan-agius4
Copy link
Collaborator Author

alan-agius4 commented Mar 30, 2022

@dgp1130, any concern about enabling experimentalDecorators internally ?

Copy link
Collaborator

@dgp1130 dgp1130 left a comment

Choose a reason for hiding this comment

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

@dgp1130, any concern about enabling experimentalDecorators internally ?

Internally, we don't allow "new" decorators (that is, ones that we aren't already invested in and required major frameworks like Angular or LitElement). I believe the intent is to avoid creating unnecessary migration problems if and when JS decorators ever land in the spec and TS decorators go away. That said, I'm ok with this particular decorator for a couple reasons:

  • This is a third party package and not developed directly in google3.
    • Typically unconformant code like this often becomes the problem of infrastructure teams which need to support it, but user code of frameworks with decorators (ex. Angular apps) will be a much bigger issue than our one decorator here and being a third-party codebase, fixing it remains our problem anyways. So we aren't adding much burden to anyone else.
  • This decorator is not exposed as a public API and is unlikely to grow significantly beyond the usage in this PR.
  • memoize is a pretty commonly used concept (particularly in web frontends thanks to React), so I think it is very unlikely that this won't be possible in a future JS decorator.
  • Even if we do need to go back on this and remove the decorator, I don't expect it would be that much effort to do so.

As a result, I think this particular decorator is probably ok. However, don't take this as carte blanche to add a bunch of decorators to the CLI. 😉

}

private _schematicCollections: Set<string> | undefined;
@memoize()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does @memoize() need to be a function? It looks like we never parameterize it. Could we rewrite it to just do @memoize?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated.

return {
...descriptor,
[descriptorPropertyName]: function (this: unknown, ...args: unknown[]) {
const key = JSON.stringify(args);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This requires that arguments are serializable to JSON. A quick look at the algorithm's explanation (unofficial) shows that non-serializable arguments (such as a function) return 'null', which could lead to surprising bugs. Ideally we wouldn't have a serializability requirement and I think that's possible with some kind of recursive Map<first param, Map<second param, Map<..., result>>>.

That would be pretty complicated, so I think we should at least document the serializability requirement and possibly check the arguments to make sure they are serializable. For example, we could JSON.stringify() each arg and make sure the result is only 'null' if the actual value is null. There might be other cases, and getters might still cause confusion, but that would probably be "good enough" safety for what we're doing here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated. PTAL

}

/** Method to check if the values are JSON serializable */
function isJSONSerializable(values: unknown[]): boolean {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems quite involved, I'm wondering if we can simplify this by just checking if each argument serializes independently? For example:

function isJSONSerializable(values: unknown[]): boolean {
  for (const value of values) {
    if (JSON.stringify(value) === undefined) {
      return false;
    }
  }

  return true;
}

This probably isn't as thorough as your implementation, since it won't catch deeper issues ([0, NaN, 1], {foo: () => {}}, ...), but I think this would be good enough to catch mistakes like memoizing a callback function.

One other nit: If the function is checking multiple values, it should probably be named areJSONSerializable(). Alternatively it could just accept one parameter and @memoize could be responsible for iterating over the arguments and checking each one.

Copy link
Collaborator Author

@alan-agius4 alan-agius4 Mar 31, 2022

Choose a reason for hiding this comment

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

There are also some edge cases which likely can be encountered. For example passing a Set as argument which isn't take unlikely.

JSON.stringify(new Set([1])) 
'{}'
JSON.stringify({}) 
'{}'

@alan-agius4 alan-agius4 reopened this Mar 31, 2022
@alan-agius4 alan-agius4 added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Apr 1, 2022
@alan-agius4 alan-agius4 removed the request for review from clydin April 1, 2022 11:27
@alan-agius4 alan-agius4 removed the action: merge The PR is ready for merge by the caretaker label Apr 1, 2022
With this change we clean up repeated caching code by creating a `memoize` decorator that can be used on get accessors and methods.
@alan-agius4 alan-agius4 added the action: merge The PR is ready for merge by the caretaker label Apr 1, 2022
@alan-agius4 alan-agius4 removed the request for review from dgp1130 April 1, 2022 16:04
@clydin clydin merged commit 3d77846 into angular:master Apr 1, 2022
@alan-agius4 alan-agius4 deleted the memoize-decorator branch April 1, 2022 16:32
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators May 2, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants