Open ept opened 7 years ago
@choxi and I put some initial exploration on the changesets branch. It looks like this should work.
Related idea: from the point of view of giving subscribers a meaningful event (not just Tesseract's hyper-granular operations list), a nice approach would be to include the Redux action object as metadata in the changeset. Maybe this could look something like the following:
// In React component that handles the user input
this.props.store.dispatch({
type: "MOVE_CARD",
cardId: cardId,
listId: this.props.listId
})
// Redux-style reducer, receives all actions from local user input
this.store = new aMPL.Store((state, action) => {
switch (action.type) {
case "MOVE_CARD":
return this.moveCard(state, action)
// this.moveCard returns a changeset object, not a new state object!
// aMPL.Store will take care of applying the changeset to produce a new state.
}
})
// Called by the reducer above, converts the action into a Tesseract changeset
moveCard(state, action) {
const cardIndex = state.cards.findIndex(card => card.id === action.cardId)
const card = state.cards[cardIndex]
const oldList = getListById(state, card.listId)
const newList = getListById(state, action.listId)
return Tesseract.changeset(state, (doc, meta) => {
Object.assign(meta, action) // copy all the fields from the action into the changeset metadata
doc.cards[cardIndex].listId = action.listId
})
}
// This subscriber now receives all changesets, regardless of whether they originated
// locally or on another device. It deliberately looks a bit like a Redux reducer, and
// since we copied the Redux action into the changeset metadata, we can access
// the action.type here as changeset.meta.type.
this.store.subscribe(changeset => {
switch (changeset.meta.type) {
case "MOVE_CARD":
// show notification or something
}
})
Here, if the application wants to include metadata in the changeset, it can mutate the second meta
parameter given to the callback. Since the first doc
parameter of the callback is already mutable, this API feels like it has a nice degree of symmetry.
The changeset metadata need not be identical to the Redux action object, and there might be good reasons for making them different. But if you want to make them the same by copying the action (Object.assign(meta, action)
), you get nice parallels between the Redux reducer (which sees only actions that originate locally) and the changeset subscriber (which sees both local and remote changesets).
An interesting usage pattern emerged in @orionz's MUD prototype. Sometimes, when a state change occurs (say, an object in the game falls to the ground with a thump), we want to not only re-render the portions of UI that depend on that bit of state (something that React handles well), but we also want to show some kind of notification event.
That notification event does not need to be part of the application state — in principle, the Tesseract document could contain a big long list of events that have occurred, but that's not really what we want, as the application would have to keep checking which of those events it has already seen. A more intuitive pattern would be to attach some metadata to the changeset itself, and for the application to subscribe to the stream of changesets being applied to the state.
At the moment, an application can watch for changesets coming in over the network by subscribing to
APPLY_DELTAS
actions. However, any changesets that originate in the local process do not go through that event loop; rather, whenTesseract.changeset()
is called from a Redux reducer, it returns a new state with the changeset already applied, and doesn't offer any sensible way for the changeset to be observed. In principle, notifications for locally originated changes could be triggered directly in the reducer, but that would quickly get messy, as the state at the time of triggering that notification would not yet reflect the changeset.Instead, maybe a better approach would route changesets through the same code path, regardless of whether they came in from the network or originated locally. That would perhaps imply that
Tesseract.changeset()
should return only a changeset (like a delta that is sent over the network), not a new state with that changeset already applied. The Redux-style reducer in aMPL could then handle locally originated changesets and deltas from the network in the same way: both would be applied to the store through anAPPLY_DELTAS
action, and both would be fed to any subscribers.Not sure what exactly the best structure for this pattern would look like in aMPL, hence opening this issue to discuss.