A mistate is a Javascript data structure with a schema, underlying immutable data structure, lensing, and efficient subscriber abilities.
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}))
},
},
})
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:
- If the return type is a type and a single argument matches a field name and type with a
@unique
directive - If the return type is a List and all arguments match fields in the type
- If an argument is
limit: Int
orskip: Int
and the return type is a list - If an argument is
xxxAfter: DateTime
orxxxBefore: DateTime
, the return type is a list, andxxx
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?
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())
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)
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]`
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]`
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))
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()
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.
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"