xaviergonz / mobx-keystone

A MobX powered state management solution based on data trees with first class support for Typescript, support for snapshots, patches and much more
https://mobx-keystone.js.org
MIT License
540 stars 23 forks source link

Design pattern: versioning #52

Open sisp opened 4 years ago

sisp commented 4 years ago

I've been thinking how to deal with model versioning. The scenario I'm looking at is saving a snapshot of the current state in local storage, so when the browser is closed and re-opened (or the app is reloaded), the previous state is automatically loaded again. All is good as long as the state layout does not change. But what happens when a snapshot is saved, I change a model class (or multiple model classes, or the entire state layout), re-deploy the app, and then the snapshot, which has the old layout, is loaded by the new state implementation? A way to migrate from one/any (old) snapshot version to the current version is needed, I think.

A couple of questions I've been asking myself:

xaviergonz commented 4 years ago

That's why "fromSnapshot" in models are there.

E.g.

// first version
@model("...")
class M extends Model({
}) {
  _version: prop(1)
  fullName: prop<string>()
} {}

// second version
@model("...")
class M extends Model({
}) {
  _version: prop(2)
  firstName: prop<string>(),
  lastName: prop<string>()
} {
  fromSnapshot(maybeOldSnapshot: any) {
    if (maybeOldSnapshot._version >= 2) {
      return maybeOldSnapshot // up to date
    }
    // update to latest
    return {
      _version: 2,
      firstName: maybeOldSnapshot.fullName.split(" ")[0],
      lastName: maybeOldSnapshot.fullName.split(" ")[1],
    }
  }
}

That way whenever an old snapshot is loaded with fromSnapshot(...) it will be updated to the latest version.

Also this way parent models don't have to worry about the versioning of its children, just about their own.

terrysahaidak commented 4 years ago

I implemented some kind of persist library with built-in support for migrations for mobx-state-tree.

The idea here is to have serialize, deserialize and migrate methods in each migration.

The serialize defines how to transform data from the snapshot to JSON. The main mission of it is resolving all the references, for example, I'm storing part of the store where I'm using some reference, in serialize I'm finding the exact entity and storing it too.

The deserialize doing vise-versa.

And the migrate transforming snapshot from v1 to v2. I'm storing the last migration index in the local store. It's pretty the same thing we do on the backend side. Each migration migrate snapshot.

With mobx-keystone, it should work even better since we have the exact model name inside each entry in the snapshot, so it might help to resolve and work with it.

sisp commented 2 years ago

See #326 for the latest API related to versioning.

PEZO19 commented 1 year ago

@xaviergonz, regarding https://github.com/xaviergonz/mobx-keystone/issues/52#issuecomment-538323617:

is there a preferred, canonical way to handle more complex scenarios?

  1. An old model is split into multiple new models
  2. Multiple old models are merged into one new model
  3. Both 1 and 2 happen in same migration, multiple occurrences