Skip to content

Instantly share code, notes, and snippets.

@donabrams
Last active May 27, 2017 17:15
Show Gist options
  • Save donabrams/b05d7ecde41f3183d29c3bfaf4336548 to your computer and use it in GitHub Desktop.
Save donabrams/b05d7ecde41f3183d29c3bfaf4336548 to your computer and use it in GitHub Desktop.
First attempt at spec-ing out a mistate data structure

mistate

A mistate is a Javascript data structure with a schema, underlying immutable data structure, lensing, and efficient subscriber abilities.

Schema

The schema format is the same as GraphQL.

Here's an example:

const state = mistate```
schema {
  query: RootQuery
  mutation: RootMutation
}

type Post {
  id: String! @unique
  title: String!
  publishedAt: DateTime!
  likes: Int! @default(value: 0)
  blog: Blog @relation(name: "Posts")
  category: Category
}

type Blog {
  id: String! @unique
  name: String!
  description: String
  posts: [Post!]! @relation(name: "Posts")
}

enum Category {
  PROGRAMMING_LANGUAGES,
  API_DESIGN
}

type RootQuery {
  blog(id: String!): Blog
  blogs: [Blog!]
  post(id: String!): Post
}

type CreatePostInput {
  title: String!
  blogid: String!
  category: Category
}

type CreateBlogInput {
  name: String!
  description: String
}

type RootMutation {
  addBlog(input: CreatePostInput): Post
  addPost(input: CreateBlogInput): Blog
  like(postId: id): Post
}```({
  mutations: {
    addBlog: (m, {add}, {name, description}) => {
      const id = m.reduce((acc, id)=>acc > id ? acc : id, 0, m.query.blogs.id) + 1
      return add(m.Blog, {
      	id,
      	name,
      	description,
      })
    },
    addPost: (m, {add}, {input: {title, blogid, category}}) => {
      const blog = m.query.blog(blogid)
      const id = blogid + "/" + m.reduce((acc, id)=>acc > id ? acc : id, 0, blog.posts.id) + 1
      return add(m.Post, {
        id,
        title,
        category,
        publishedAt: new Date(),
        blog,
      })
    }),
    like: (m, {update}, {postId}) => {
      return update(m.Post, {id: postId}, ({likes}) => ({likes: likes + 1}))
    },
  },
})

Autogenerated queries

The object we pass the string template schema is the resolver for mutations and queries that can't be autogenerated.

Queries can be autogenerated in the following circumstances:

  1. If the return type is a type and a single argument matches a field name and type with a @unique directive
  2. If the return type is a List and all arguments match fields in the type
  3. If an argument is limit: Int or skip: Int and the return type is a list
  4. If an argument is xxxAfter: DateTime or xxxBefore: DateTime, the return type is a list, and xxx is a field in the type

Mutations cannot be autogenerated (as this would lead to CRUD, a situation this data structure exists to avoid).

So cool, it has a schema. How do we use it, ie get data out and persist data in?

Creating/initializing a mistate

First, let's create an instance of the state we defined above.

const m = state()

We can serialize a mistate instance at any time and create simple, valid JSON

const m = state()
m.serialize() // {"Posts": [], "Blogs": []}

We can also initialize a mistate with this serialized state

const m = state.deserialize({Posts: [], Blogs: []})

Finally, we can "copy" another mistate

const m = state(state())

Reading and updating a mistate

Accessing a mistate is NOT like a accessing a normal javascript object. Each . returns a lens. Using the lens, you can use crud and list operations like so.

const m = state()
const idLens = m.query.blogs.id
console.log(m.get(idLens)) // []  !!TODO: or should this be undefined??
const b = m.mutation.addBlog({name: "foo", description: "bar"}) // Blog
console.log(m.get(idLens)) // [1]
console.log(m.get(b.id)) // 1
console.log(m.reduce((acc, id)=>acc+id, 0, idLens)) // 1

Note that the mutation above DOES mutate the original state. A mistate is NOT an immutable data structure (though it uses one underneath)

Subscribing to changes

The second really awesome part of a mistate is the subscription API. You can register a listener that fires when the data in a lens changes.

const m = state()
const idLens = m.query.blogs.id
m.subscribe(idLens, (ids) => console.log(ids)) // logs `[]`
m.subscribe(idLens, (ids) => console.log(ids), {initial: false}) // does NOT log `[]`
m.mutation.addBlog({name: "foo", description: "bar"}) // logs `[1]`

Race conditions

When setting up an application, race conditions can happen pretty often. In this case, use replay which will fire all the mutation events mistate has made in order. A mistate keeps a small history of previous states around since the underlying immutable data structure doesn't take up much space per change.

TODO: this seems vague

const m = state()
const idLens = m.query.blogs.id
m.mutation.addBlog({name: "foo", description: "bar"})
m.mutation.addBlog({name: "beep", description: "boop"})
m.subscribe(idLens, (ids) => console.log(ids), {replay: true}) // logs `[1]` then logs `[1, 2]`

Multiple lenses in a subscription

If you want a single listener to more than one piece of state, you can do it:

const m = state()
const postCountLens = m.query.blogs.count
const blogCountLens = m.query.posts.count
m.subscribe([postCountLens, blogCountLens], 
    ([postCount, blogCount])=>console.log(postCount, blogBount))

Unsubscribing

To unsubscribe a listener, execute the unsubscribe function m.subscribe produces.

const m = state()
const idLens = m.query.blogs.id
const unsubscribe = m.subscribe(idLens, (ids) => console.log(ids))
unsubscribe()

Throttling

All listeners are fired off asynchronously. This is a pretty natural batching mechanism. But sometimes you only want listeners to only fire every so often. In this case, you can set a "global" throttle on a mistate.

const m = state()
m.throttleSubscriptions(1000) // fire at most once a second
const idLens = m.query.blogs.id
m.subscribe(idLens, (ids) => console.log(ids)) // logs `[]`
setInterval(() => m.mutation.addBlog({name: "beep", description: "boop"}), 200)
// a second later, logs `[1]`,
// then a second later logs `[1, 2, 3, 4, 5, 6]`, 
// then a second later logs `[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]`, 
// and so on.

You can also set a throttle on a specific subscription, though this is likely inefficient. There is also a debounce.

Errors in listeners

You can handle errors in all listeners by setting up a handler. By default, mistate rethrows listener errors. If you need to handle an error in a single listener, do it in the listener.

const m = state()
m.onError((e)=>console.log(e))
const idLens = m.query.blogs.id
m.subscribe(idLens, () => throw "boo") // logs "boo"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment