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

Parcel 2: Serializer for complex object types #2522

Merged
merged 10 commits into from
Jan 12, 2019
Merged

Conversation

devongovett
Copy link
Member

This PR adds a serializer for complex object types (e.g. classes with prototypes) to JSON and back. It is used in the cache and workers in order to ensure that objects such as Asset, Dependency, Environment, AssetGraph, etc. get properly recreated. This all mostly happens automatically under the hood, so is easily extensible for future objects.

There are two parts to the implementation:

  1. A babel plugin, which adds an __exportSpecifier static property to classes which are exported from modules. This property stores the filename the object came from (relative to the package so it is portable, e.g. @parcel/core/lib/Asset), and the name of the export (e.g. Asset or default). It allows the runtime to inspect objects as they are being serialized and save the type information so that they can be restored.
  2. A runtime, with serialize and deserialize functions. serialize uses a JSON.stringify replacer function to add a $$type property to objects that have an __exportSpecifier property on their constructor. deserialize uses this property to require the file and access the constructor to recreate the object.

While this will work automatically for many objects, there are some hooks to control how objects get serialized. If there is a serialize method on the object, it will get called prior to serialization. This is similar to the toJSON method that JSON.stringify normally calls. If there is a static deserialize method, it will also get called instead of the constructor to recreate the object.

The serializer enables a few things, which have also been done in this PR.

  • An Environment class, which has some methods to get computed data about the environment, like if it is a node or browser environment, if it's isolated, etc. We could add more computed info here in the future.
  • A Dependency class to replace the createDependency helper

In order to make this work, I also had to make the Cache a singleton. This allows the Asset class to access the cache without one being passed in - it wouldn't when deserialized. In reality, there was already a single instance per worker, it was just passed around, so this isn't that big of a change.

@jamiebuilds
Copy link
Member

This makes me nervous. It seems like every class that needs to be serialized/deserialized to/from the cache should have a JSON intermediary value that the classes can output and be created from:

interface AssetCacheValue {
  foo: string
  bar: number
}

class Asset {
  static createFromCacheValue(value: AssetCacheValue) {
    return new Asset(...)
  }

  toCacheValue(): AssetCacheValue {
    return {
      foo: this.foo,
      bar: this.bar,
  }

  ...
}

While this is a lot of manual work, it's also faster and much less error prone. Deep cloning values correctly in JavaScript is so complex that TC39 has rejected it multiple times. You're going to create much weirder errors from the side-effects that will inevitably be triggered from restoring values from a serialized version.

@devongovett
Copy link
Member Author

devongovett commented Jan 7, 2019

That's basically what this is doing. You can implement serialize and deserialize on your class and return a JSON value. If you don't do that, then it uses the default JSON serialization. JSON.stringify and JSON.parse basically handle the recursion for us, so by the time one object is deserialized all of the children have already been.

Either way, you need a way to know given a JSON object what type it was originally serialized from in order to call the createFromCacheValue/deserialize method on that type.

We could remove the support for automatically serializing/deserializing classes without a serialize/deserialize method I guess, but it seemed convenient to be able to do that automatically. 🤷‍♂️

@@ -25,13 +25,12 @@ type AssetOptions = {
type: string,
code?: string,
ast?: ?AST,
dependencies?: Array<Dependency>,
dependencies?: Array<IDependency>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there ever a time where one of the dependencies would not be an instance of the Dependency class? Do we expect something other than the Dependency class to implement the interface? Just wondering if the interface is necessary. Not a blocker though.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well... I don't think so, but flow was complaining since the types in @parcel/types refer to the interface not the class. Is there a better way?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, right. Not sure of another way besides pulling those classes out of core, which we didn't want to do. So yeah, this seems like the best option.

@@ -57,12 +57,25 @@ export type EnvironmentContext =
| 'service-worker'
| 'node'
| 'electron';
export type Environment = {

export type EnvironmentOpts = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor nitpick, but I noticed a discrepancy between EnvironmentOpts and DependencyOptions. Should we pick one or the other?

Copy link
Contributor

@padmaia padmaia left a comment

Choose a reason for hiding this comment

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

Made a few comments, but nothing blocking. Looks good.

@devongovett devongovett merged commit ee00c3e into v2-work-so-far Jan 12, 2019
@devongovett devongovett deleted the serializer branch January 12, 2019 17:39
@devongovett
Copy link
Member Author

Merging. If it ends up causing problems we can always change it.

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.

4 participants