vigetlabs/microcosm

Add a way to alias actions in Domains

Closed this issue · 6 comments

I want a better way to alias actions with string names. This would be a breaking change because it would rework the way string actions are treated.

I would like to consider putting actions right next to domains. A domain module would contain both actions and handlers.

export function createUser (params) {
  return axios.post('/users', params)
}

class UsersDomain {
  actions = { createUser }

  add(users, user) {
    return users.concat(user)
  }

  register() {
    return {
      createUser: this.add
    }
  }
}

let repo = new Microcosm()

repo.addDomain('users', UsersDomain)

repo.push('createUsers', { name: 'Bill' }) // adds a new user
repo.push('createUser', { name: 'Bill' }) // Error: Unrecognized action "createUser". Did you mean "createUsers"?

When a domain is added it will:

  1. Validate that no existing aliases exist for the domain
  2. Tag the action with the associated key
  3. If a string action is pushed and nothing responds, the action will reject with a standard error
  4. If no code subscribes to "action.onError", Microcosm will send out a warning

Things to figure out

  1. Can we do this for Effects too?
  2. What happens when two domains alias the same action?
  3. If we declare actions ahead of time, Microcosm can know about them. This would let us make a "control box" in the debugger for firing off actions.

I don't immediately see the benefit of this. Just to have actions and domains live next to each other? "alias actions with string names"?

TLDR: I want to build up Microcosm's level of abstraction so that users can write even less boilerplate. To do that, it would be very helpful to know more about what they are trying to do.

But please read 😸


Longwinded response:

One benefit is that we could remove "tagging" magic (though we'd keep it for backwards compat, you could avoid it). The keys of the action namespace on the domain could be used as the identifiers. I could see a domain like:

import uid from 'uid'

repo.addDomain('planets', {
  initialState: [],
  actions: {
    addPlanet: name => { id: uid(), name }
  },
  register: {
    addPlanet: (planets, record) => planets.concat(record)
  }
})

If we provided more helpers out of the box, something I'd like to do, this could eventually be:

import { makeRecord, appendList } from 'microcosm'

repo.addDomain('planets', {
  initialState: [],
  actions: {
    addPlanet: makeRecord
  },
  register: {
    addPlanet: appendList
  }
})

I also want to know all possible actions ahead of time. This is useful for tooling. We'd know ahead of time every single action and what domains registered to them.

This would be particularly useful for https://github.com/vigetlabs/draft-microcosm-graphql, so that I can build up a nice set of layers with escape hatches:

  1. At the top, a Microcosm is defined in GraphQL schema
  2. That schema builds domains, exposing what actions/registrations are possible.
  3. Those domains can be extended, or custom implementations of specific GraphQL queries/mutations can override sensible defaults

food for thought, typical disclaimer about how my ideas might be off base:

  • "I also want to know all possible actions ahead of time."
    • is that not something we can do already with Domain's register method?
  • import { makeRecord, appendList } from 'microcosm'
    • that sure is neat looking, but does it overly assume that domains are typically used for managing very simple data structures? and thus does it become not useful the moment you want to do something slightly more complex and unique to your application.
  • this thing:
repo.addDomain('planets', {
  initialState: [],
  actions: {
    addPlanet: makeRecord
  },
  register: {
    addPlanet: appendList
  }
})

i feel like is the first step on the slippery slope leading towards you saying, "I want this to happen:"

repo.addDomain('planets', {
  actions: [create, read, delete]
})

That last one may be a stretch of a comment, but I guess my general thinking is that while removing boilerplate is indeed useful, it also hides a layer of functionality, so we better make sure we're okay with hiding it.

is that not something we can do already with Domain's register method?

No, because I want the actual action function. A Domain's register method only has access to the action's unique identifier and the associated handler.

  import { makeRecord, appendList } from 'microcosm'

That sure is neat looking, but does it overly assume that domains are typically used for managing very simple data structures?

It might be a bad idea. Almost all of the domains I have seen have handlers along the lines of:

  1. Add an item to a list
  2. Remove an item from a list
  3. Update some fields on an item in a list
  4. Set a field in an object
  5. Toggle a boolean field in an object
  6. Reset a field in an object to the original value, usually null

I'd be fine holding off on that, or throwing away the idea entirely.

i feel like is the first step on the slippery slope leading towards you saying, "I want this to happen:"

  repo.addDomain('planets', {
   actions: [create, read, delete]
 })

I agree that this is too far. However I think this is too much abstraction not because of premade functions for manipulating data, but because it obfuscates the integration points between actions and domain handlers. A user of Microcosm should not have difficulty controlling the flow of their program.

But still, appendList instead of (list, item) => list.concat(item) is hiding work for little benefit. I agree that we should only do this if it solves real problems and avoids pitfalls.

👍 seems like you have your head on straight about this. i will follow you into the dark!

Aww. I promise to bring flashlights and snacks.