-
Notifications
You must be signed in to change notification settings - Fork 41
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
onChange is triggered when elements are removed #155
Comments
Hi @ipsquiggle, I apologize for the inconvenience but this is currently the intended behavior - reference on migration guide. I'm still not very happy with how the callback API works. It feels something is off by calling methods directly from what was supposed to be raw data structures. I'd love to detach the callback API from the data structures themselves in the future. Suggestions are very welcome. Cheers! |
I see it in the guide, thanks for the update! Agreed, that part of the API is awkward, and I've spent some time thinking about it. The problem seems to be that you need "existing data" to be able to define the callbacks on that data (or children of it). Two alternatives keep coming to mind, though neither is obviously better, but for the sake of discussion:
|
Hello guys! The colyseus client could have an EventsManager instance that will emit sync events for every message received, for example:
The main point here will be to use the most simple and intuitive name convention for the messageKey, like:
Or:
Other benefits will be:
|
@damian-pastorini Makes sense to me. My main concern would be what this looks like for collections of complex elements, and how filtering might look? First case:
The first key triggers when the It gets a bit necessarily funny when things nest:
Does that make sense? Is there a simpler way of doing this that I'm not seeing? The second case is filtering. If I want to listen to only the first player's items for whatever reason, I need to either: only register the listener on a filtered portion of the tree, or, have enough context in the handler to react properly. I can't actually even imagine what the second case looks like. Here's some imaginings of what filtering might look like The key could accept direct mappings into collections as a filter:
Alternatively, we could provide a filter function that slices up the key:
Just thinking out loud here in the hope that it helps. Throughout all of this, type safety is high on my mind, I'm quite dubious that it can be achieved in this model, but hopefully at least immediate runtime validation could be performed? |
hi @ipsquiggle , ok, let's tackle one by one 😄 As for the first items, nested properties, I'm not sure why you are adding those "*", remember these are listeners on the client, it doesn't require to filter any specifics, we are talking about how to avoid the current callbacks implementation.
As you can see you will always need to know in advance the properties to be listened, but that's already the case on 0.15 version and so you can have a pre-build list of properties defined to be listened on the client, or you can just go crazy and:
But if you need to go deeper you could in the exact same way:
As for the filter... I think you are mixing server with client side? You would filter on the server, the client would only receive the updates that will regard to that client, so when you listen for the events on the client you can keep using the events noted above. Or I'm missing something here? Thanks! |
Concerning the Star syntax in the paths The reason for the * is as mentioned: to distinguish listening to the field containing the array, vs the contents of the array. I see I caused confusion by introducing filters, so let's forget about filters for the moment. Consider these actions on the server, and how you'd distinguish them on the client:
Existing 0.15 handling of these:
I don't enjoy the syntax of the
This distinction is needed because the collection itself is an object with a lifecycle we care about. I worry that there's been another distinction lost here, which is that we've conflated the entries-as-a-set with the entry-itself. I don't think it matters, but it maybe does. (e.g. In your examples, you listen to the Player object directly.. this may work most of the time, but not always. For example, I have a I'll be honest, the more I think about this the more it scares me. It's definitely worth exploring, and I really would like to decouple the events from the objects. But it's not obviously better. :( |
Ok, I think you are mixing how schemas and properties works, consider it like this:
That been said, you can easily know what and where you need to listen, even if it's an schema inside another, or map inside a map, etc. Here some examples:
If I'm missing any case here please show me and I would translate it to events, so for those cases you would listen to:
Even if you have something like the InventorySchema used in another object, you can still listen to the events for the same object: colyseus.listen('npc.inventory.add', (itemId, itemData) => {}); For me it would be incredible simple using events, I really hope this kicks in :D |
Indeed, there is a lot of listener creation necessary. When something is ADDED to a MapSchema or ArraySchema, it is common you then need to add listeners for changes on the newly added item. I think a big consideration should be Closures (and how to avoid them). Probably the ability to pass in a context. We have to use closures now, which captures the full call stack and frankly just burns up memory. Being able to set a context would save a lot of memory on the closures. For example, if each PlayerSchema includes a MapSchema, I want a listener on the MapSchema for every player, and I need to add a listener for every Schema within the map schema. So, during the Using a system where we have a single listener for these changes, like |
Thank you so much for sharing your thoughts and ideas @ipsquiggle @damian-pastorini @hunkydoryrepair 💙 PS: I've just posted a big roadmap for v3 of schemas here 🙌 colyseus/colyseus#709 |
Hi guys! So, I've been banging my head for a few days trying to come up with an API, and so far, this seems to be achievable and not too different from what we currently have. (I'm not 100% sure this is viable, I'm currently experimenting with the actual implementation...) This implementation would use proxies to attach callbacks to structures located at a path in the state. (It brings nostalgic jQuery feelings...) $ = getStateCallbacks(room); // The SDK may provide this as "room.$"
$(room.state) // returns a proxy
$(room.state).teams // returns a proxy
$(room.state).childSchema // returns a proxy There are 2 types of proxies: a proxy to a collection and a proxy to a schema instance:
onAdd / onRemove in "collections"$(state).teams.onAdd((team, index) => {/* ... */});
$(state).teams.onRemove((team, index) => {/* ... */}); The Alternatively, if the $(state.teams).onAdd((team, index) => {/* ... */});
$(state.teams).onRemove((team, index) => {/* ... */}); deep structuresThe same pattern we're used to having would apply here: $(state).teams.onAdd((team, index) => {
$(team).entities.onAdd((entity, entityId) => {
// entity is the actual instance!
});
}); New method proposal:
|
Hi there, room.state.users.onAdd((u) => {
dispatch(addUser(u))
const fields: NonFunctionPropNames<User>[] = ["x","y"]
fields.forEach((field) => {
u.listen(field, (value) => {
dispatch(changeUser(u, field, value))
}
})
})
room.state.users.onRemove((u) => {
dispatch(removeUser(u))
}) multiplied X times the number of schemas i have. If my schema contains nested schemas, i have to nest the same callback-to-action-dispatch pipeline for each nested schema. So obviously i'm much interested in the |
We've just upgraded to the most recent version of colyseus, and noticed we were getting lots of null exceptions in the
onChange
handlers on the client.An investigation revealed that this line has changed from
else if
to justif
.schema/src/Schema.ts
Line 1028 in aef7a9a
This means that when an element is removed, the
onRemove
fires (withpreviousValue
for the value), thenonChange
fires withvalue
as the value -- which at this point is definitely null.This defies my intuition about the meaning of these callbacks; the fact that they are two separate callbacks (instead of e.g. just an
onChange(currentVal, previousVal, key)
) makes me expect thatonChange
would always have a validvalue
passed in.It's also surprising because previously I had to listen to all three call backs (add, change, remove) to monitor the full lifecycle of objects, but now if I do that I get two callbacks when an item is removed.
Ultimately, just looking for guidance on whether this is a bug or the new design, and I will update our code accordingly. :)
The text was updated successfully, but these errors were encountered: