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

refactors, onInvalidation hook, types.safeReference and more #1091

Merged
merged 30 commits into from
Dec 3, 2018

Conversation

xaviergonz
Copy link
Contributor

@xaviergonz xaviergonz commented Nov 24, 2018

Closes #1088 #1086 #1081

  • small refactor over references
  • small refactor over scalar and object nodes, by extracting common code to a BaseNode class
  • added an internal mechanism to subscribe to hooks (all of them for object nodes, afterAttach/beforeDestroy for scalar nodes)
    • this should make it kind of trivial to make a addHook(model/array/map/scalar/whateverType, 'afterCreation'..., (self) => {...}) in the future if desired
  • added an onInvalidated option to the reference options (explained below)
  • added a types.safeReference type (explained below)
  • note that isValidReference and tryReference were alredy merged in tryReference and isValidReference #1087 this just adds extra docs for them

Reference validation: isValidReference, tryReference, onInvalidate hook and types.safeReference

Accessing an invalid reference (a reference to a dead/detached node) triggers an exception.

In order to check if a reference is valid, MST offers the isValidReference(() => ref): boolean function:

const isValid = isValidReference(() => store.myRef)

Also, if you are unsure if a reference is valid or not you can use the tryReference(() => ref): ref | undefined function:

// the result will be the passed ref if ok, or undefined if invalid
const maybeValidRef = tryReference(() => store.myRef)

The options parameter for references also accepts an optional onInvalidated hook, which will be called when the reference target node that the reference is pointing to is about to be detached/destroyed. It has the following signature:

const refWithOnInvalidated = types.reference(Todo, {
    onInvalidated(event: {
        // what is causing the target to become invalidated
        cause: "detach" | "destroy" | "invalidSnapshotIdentifier"
        // the target that is about to become invalidated (or undefined if the cause is "invalidSnapshotIdentifier")
        invalidTarget: STN | undefined
        // the identifier that is about to become invalidated
        invalidId: string | number
        // parent node of the reference (not the reference target)
        parent: IAnyStateTreeNode 
        // a function to remove the reference from its parent (or set to undefined in the case of models)
        removeRef: () => void 
        // a function to set our reference to a new target
        replaceRef: (newRef: STN | null | undefined) => void 
    }) {
        // do something
    }
})

Note that invalidation will only trigger while the reference is attached to a parent (be it a model, an array, a map, etc.).

A default implementation of such onInvalidated hook is provided by the types.safeReference type. It is like a standard reference, except that once the target node becomes invalidated it will:

  • If its parent is a model: Set its own property to undefined
  • If its parent is an array: Remove itself from the array
  • If its parent is a map: Remove itself from the map

Strictly speaking it is implemented as

types.maybe(types.reference(Type, {
    ...customGetSetIfAvailable,
    onInvalidated(ev) {
        ev.removeRef()
    }
}))
const Todo = types.model({ id: types.identifier })
const Store = types.model({
    todos: types.array(Todo),
    selectedTodo: types.safeReference(Todo)
})

// given selectedTodo points to a valid Todo and that Todo is later removed from the todos
// array, then selectedTodo will automatically become undefined

@xaviergonz xaviergonz self-assigned this Nov 25, 2018
@xaviergonz xaviergonz added the enhancement Possible enhancement label Nov 25, 2018
@xaviergonz xaviergonz mentioned this pull request Nov 25, 2018
2 tasks
@xaviergonz xaviergonz changed the title reference, onInvalidation hook, types.safeReference and more refactors, onInvalidation hook, types.safeReference and more Nov 25, 2018
@xaviergonz
Copy link
Contributor Author

xaviergonz commented Nov 25, 2018

Renamed safeReference to weakReference, it seems like a better name

@xaviergonz xaviergonz changed the title refactors, onInvalidation hook, types.safeReference and more refactors, onInvalidation hook, types.weakReference and more Nov 25, 2018
@xaviergonz
Copy link
Contributor Author

all done

@k-g-a
Copy link
Member

k-g-a commented Nov 27, 2018

Do you think it can be that bad?

I think that one level won't be noticable at all, but I am not sure, need measurement.
Will try some profiling next week. But I think that this point should not block PR from being merged!

wouldn't that throw upon access? the refs and the source reside in a different "tree"

I've just discovered, that this code was perfectly valid only up to 3.7.0 (including, https://codesandbox.io/s/vmq29xm1yl) - 3.7.1 broke it. Ability to have multiple trees is one of MST's top features. Reusing nodes from one tree in another was a bit of a pain, but it worked. Could we bring that behaviour back?

Found a way to do it without actually instantiating references (at least for the case without the custom get/set)

Didn't get that point. If one's reference source resides in another tree he must supply instances on create, not identifiers/snapshots. Othervise IdentifierCache should have been global and shared between all root nodes.

const Source = types.model({items: types.array(MyType)})
const Refs = types.model({linkedItems: types.array(types.reference(MyType))})
const source1 = Source.create({items: [{id: 1}, {id: 2}, {id: 3}]})
const source2 = Source.create({items: [{id: 1}, {id: 4}, {id: 5}]})
// at this point we could not supply ids, because id=1 lives in both source1 and source2
const refs = Refs.create({linkedItems: source1.items})

@xaviergonz
Copy link
Contributor Author

xaviergonz commented Nov 27, 2018

@k-g-a

I think that one level won't be noticable at all, but I am not sure, need measurement.
Will try some profiling next week. But I think that this point should not block PR from being merged!

Fair enough :)

I've just discovered, that this code was perfectly valid only up to 3.7.0 (including, https://codesandbox.io/s/vmq29xm1yl) - 3.7.1 broke it. Ability to have multiple trees is one of MST's top features. Reusing nodes from one tree in another was a bit of a pain, but it worked. Could we bring that behaviour back?

I didn't even know that was supposed to be possible. I guess that's something for @mweststrate to decide?

Didn't get that point. If one's reference source resides in another tree he must supply instances on create, not identifiers/snapshots. Othervise IdentifierCache should have been global and shared between all root nodes.

Well, my answer was assuming that references and referenced nodes had to share a common root. Of course if they don't there's no way check if references are valid at creation time.

Just wondering, if refs can be assigned to referenced nodes in other stores, what's an id assignation default behaviour? taking your example code, what should this do?

refs.linkedItems[0] = "5" // some id... from which store... ??

Also, if you restore a snapshot, how will the reference know into which store it should hook to find the node?

@xaviergonz xaviergonz added the require('@mweststrate') @mweststrate input is required! label Nov 27, 2018
@xaviergonz
Copy link
Contributor Author

Hmm the docs say https://github.com/mobxjs/mobx-state-tree#references

References are looked up through the entire tree but per type, so identifiers need to be unique in the entire tree.

@k-g-a
Copy link
Member

k-g-a commented Nov 28, 2018

taking your example code, what should this do?

Throw ))
I know that using reference source from other tree is a kind of a hack and is only possible in limited scenarios. I won't even argue if this will be forbidden: there is always an alternative way through volatile state from env :)
If we really want to allow such approach, we could make shared IdentifierCache: this would be breaking change. Or won't if we make it under option or as a separate type. Anyway, I think that it's for @mweststrate to decide.

@mweststrate
Copy link
Member

Renamed safeReference to weakReference, it seems like a better name

Wouldn't this be confusing with the weakReference proposal in javascript? https://github.com/tc39/proposal-weakrefs/blob/master/specs/weakrefs.md

@mweststrate
Copy link
Member

taking your example code, what should this do?

Throw ))
I know that using reference source from other tree is a kind of a hack and is only possible in limited scenarios. I won't even argue if this will be forbidden: there is always an alternative way through volatile state from env :)
If we really want to allow such approach, we could make shared IdentifierCache: this would be breaking change. Or won't if we make it under option or as a separate type. Anyway, I think that it's for @mweststrate to decide.

The idea I had was that standard types.reference doesn't work for different trees, however, that custom references would be able to resolve inside a different tree, by using resolveIdentifier and such:

types.reference(User, {
        // given an identifier, find the user
        get(identifier) {
            return resolveIdentifier(someOtherStore /* this is another tree, accessed from closure, import, env or whatever */ , User /* type (living in the other tree) */, identifier)
        },
        // given a user, produce the identifier that should be stored
        set(value /* User */) {
            return value.name
        }
    })

@mweststrate
Copy link
Member

@xaviergonz didn't review the code yet, since not all conceptual questions are answered :-). But this PR looks like a great improvement!

@xaviergonz
Copy link
Contributor Author

xaviergonz commented Nov 30, 2018

@mweststrate Thanks for the feedback :)

Wouldn't this be confusing with the weakReference proposal in javascript?

Didn't even know that existed. I'll switch it back to safeReference then

The idea I had was that standard types.reference doesn't work for different trees, however, that custom references would be able to resolve inside a different tree, by using resolveIdentifier and such:

Ah, so it is supposed to be ok for custom refs.
Ok, then I'll get rid of the "invalidSnapshotIdentifier" cause (since that needs snapshots of the reference targets on the same tree than references themselves) and add some tests for custom refs that point to other stores.

Thanks!

@xaviergonz xaviergonz changed the title refactors, onInvalidation hook, types.weakReference and more refactors, onInvalidation hook, types.safeReference and more Nov 30, 2018
@xaviergonz
Copy link
Contributor Author

xaviergonz commented Nov 30, 2018

Ok, I was able to keep the invalidSnapshotIdentifier even for custom identifiers in another stores

(basically in this case upon creation of the safe reference, it will try to access the node, thus trigger the getter, and if that getter does not return a node then the reference will trigger the invalidSnapshotIdentifier cause)

Also I wrote a unit test to make sure that custom identifiers can use nodes from external trees (it seems it keeps working)

@xaviergonz
Copy link
Contributor Author

Also renamed it back to safeReference

@xaviergonz
Copy link
Contributor Author

xaviergonz commented Nov 30, 2018

const Source = types.model({items: types.array(MyType)})
const Refs = types.model({linkedItems: types.array(types.reference(MyType))})
const source = Source.create({items: [{}, {}, {}]})
const refs = Refs.create({linkedItems: source.items})

@k-g-a added a unit test for that, but using custom references (it works)

Copy link
Member

@mweststrate mweststrate left a comment

Choose a reason for hiding this comment

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

Awesome work!

@xaviergonz
Copy link
Contributor Author

xaviergonz commented Dec 3, 2018

Thanks! last thing, renamed oldRef to invalidTarget and added invalidId to the event

@xaviergonz xaviergonz merged commit a15d516 into master Dec 3, 2018
@k-g-a
Copy link
Member

k-g-a commented Dec 5, 2018

@xaviergonz, thanks for great work! Could you estimate how soon will it land? :)

@xaviergonz
Copy link
Contributor Author

I'll make a new release this afternoon

@xaviergonz
Copy link
Contributor Author

@k-g-a 3.9.0 released

@k-g-a
Copy link
Member

k-g-a commented Dec 7, 2018

@xaviergonz, refactored our production code from volatile state to safeReferences: it works great and reduces boilerplate. Thanks for contribution!

@xaviergonz
Copy link
Contributor Author

@k-g-a my pleasure :D

@xaviergonz xaviergonz deleted the safeReference branch December 29, 2018 18:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Possible enhancement require('@mweststrate') @mweststrate input is required!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants