Code Monkey home page Code Monkey logo

arbor-store's People

Contributors

drborges avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

arbor-store's Issues

Implement arbor-model

The idea here is simple, allow developers to provide their own abstraction to a given state tree path. Take the following Store as example:

const store = new Store({
  todos: [
    { title: "Do the dishes", status: "todo" },
    { title: "Take the dog out", status: "doing" },
    { title: "Do the laundry", status: "done" },
  ]
})

One could then create a class to represent a todo and even provide a more domain specific API:

@Model("/todos/:index")
class Todo {
  isStarted() {
    return this.status === "doing"
  }

  start() {
    if (this.status === "todo") this.status = "doing"
  }

  finish() {
    if (this.status === "doing") this.status = "done"
  }
}

This model can be made visible to the store by "registering" it as so:

import { Todo } from "./models"
store.register(Todo)

With that, whenever a todo is accessed, an instance of Todo would be provided:

const todo = store.todos.find(todo => !todo.isStarted())
todo.start()

Use WeakMap for cache implementations

Arbor uses a few different caches in order to optimize memory usage and speed things up a bit. For example, there is always a single instance of Path for a given sequence of path nodes, e.g. new Path("users", 0) === new Path("users", 0).

Each proxy within a PTree has a cache to hold their children so that they are only created once (upon first access) for a given state tree, e.g. multiple accesses to the users node (tree.root.users) create the users proxy as a child of root on the first access, subsequent accesses yield the cached proxy.

Using WeakMap for these caches would (likely, benchmark might be needed) allow the GC to free up memory when no other object in the app is referencing these cached items.

Allow restoring a tree path to a previous value

This would essentially allow one to "rollback" mutations to a certain path. For optimistic UIs this means we'd be able to easily rollback UI changes upon API request failures:

store.subscribe("/users/:index", async (newUser, oldUser, restore) => {
  const response = await api.users.update(newUser)
  if (response.status >= 400) {
    restore(oldUser).errors.push(response.message)
  }
})

Subscribers would receive a restore function as the third argument, to be called with the previous value of the mutated node, returning the restored node allowing further method chaining.

Optimize final package size

  1. Remove unnecessary code such as index.js files only used internally by the lib
  2. Remove core.js and babel-runtime from the final packages.

Provide old and new state values to subscribers

Currently, subscribers are notified with only the new state. The old state should also be provided so that subscribers may choose to, perhaps, rollback changes in case it is needed.

store.subscribe("/users/:index", async (newUser, oldUser, restore) => {
  const response = await api.users.update(newUser)
  if (response.status >= 400) {
    restore(oldUser).errors.push(response.message)
  }
})

This would be very useful for implementing optimistic UIs, where local changes are applied right away and may be rolled back depending on the server's response.

Tree subscriptions seem to receive a tree root with non-refreshed children references

On the example below, the first subscription logs out an invalid state for the store, where name still says Diego. The second subscription, though, logs out the state properly, which leads me to believe that the Tree#root is not updated properly upon mutations.

const store = new Store({
  user: {
    name: "Diego",
    age: 32,
  }
})

store.user.name = "Bianca"

store.subscribe("/", state => console.log(state))
store.subscribe("/", state => console.log(state.user))

Allow for async initial store state

This would allow for creating a Store with an initial async state, likely provided by an API (GraphQL??)

const promise = fetch("my.api.com/state")
const store = new Store(promise)
store.subscribe(console.log)

Subscribers should be notified with the current state upon subscription

Currently, subscribers are only notified of mutations when they happen and never have a chance to handle the current store state upon their subscription.

This would allow for instance React dev tools to correctly show the current app state on its first rendering cycle (before any user interactions have taken place).

Update README file

The current README file is outdated and needs to be updated to reflect the new Arbor API (subscriptions).

  • Add Installation section
  • Add React section (quick overview and link to arbor-react)
  • Update Timetravel section to point to arbor-timetravel repo
  • Add Examples section with working example apps (add arbor-react-app to the examples list)
  • Add License
  • Add How to Contribute section
  • Add more examples
  • Add a section to provide a good overview of how Arbor works internally (State Tree, Structural Sharing, Mutation Paths, Transaction and large dataset updates, etc...)
  • Add a section for benchmark results (add graphs, everybody likes graphs :)

Arbor-devtools

Every framework/lib is as good as the tools supporting its development. A devtools plugin for (initially) chrome would help to reduce the adoption friction of Arbor. Here are some ideas for features:

  1. Time travel out-of-the-box: Anyone should be able to choose to start recording state mutations on an Arbor app and be able to replay them, or travel back and forth in time, and export the recording so other user may import and replay those mutations on their machine.
  2. 3D timeline to represent app mutations over time. Each mutations would render the entier app on a given z-index, allowing users to move back and forth in time, visualizing the different state snapshots taken during that period.
  3. Live state tree rendering and path access highlighting. There should be a panel within the devtool where a tree data structure can be rendered to represent the current app state. As the dev access state within the tree via the dot notation API (store.state.users[0].name) the accessed path within the tree is highlighted.

Implement arbor-timetravel package

This package would be a proof-of-concept project to validate whether or not arbor would provide a nice way to handle time travel functionality for any given app.

Basically, this package would allow one to travel back and forth in time and see all the state mutations that happened in that period.

Prototype:

import Store from "arbor"
import withTimetravel from "arbor-timetravel"

const StoreWithTimetravel = withTimetravel(Store)
const store = new StoreWithTimetravel({
  todos: []
})

store.timeline.back(1)
store.timeline.forward(1)
store.timeline.last()
store.timeline.first()

Better test coverage

  • add specs to show that tree nodes (proxies) support es6 features such as destructuring objects (NodeObject) and arrays (NodeArray).
  • add specs to NodeArray to make sure the Array API (slice, map, reduce, filter, find, etc) will behave as expected when called on the tree node proxies.
  • refactor specs so they use a common theme (todo app for instance) for better consistency.
  • refactor spec examples so they focus on a single (if possible) aspect of the feature.

Subscribe to specific mutation paths

Currently, subscribers are notified of any state change, receiving the new state tree root node as argument.

The idea is to allow subscriptions to created for specific mutation paths, e.g.:

// Subscribe to new posts being pushed into a user's posts list
store.subscribe("/users/:index/posts/push", (post) => {
  // do something with new post
})

// Subscribe to new posts being pushed into a user's posts list
store.subscribe("/users/:index/name", (user) => {
  // do something with user whose name was just updated
})

We might want to hold off on this feature until there are more clear use cases.

Support #destroy operation

It would be quite convenient if we could easily remove branches of the state tree by simply calling node.$destroy().

If node is a child of an object, the operation would be delete parent[nodeProp]. In case the node is an array item, the operation can be fulfilled by Array.splice.

Remove elements from arrays is a common operation in React apps which should make a case for this API. Removing props from objects seems a little less common, but could still be handy should the user decide to build normalized key/value stores.

Implement `Node#transaction` operation to allow "atomic" mutations

Currently, every mutation operation (set, Array#push, etc...) will notify all subscribers as soon as they are done running. Because of that, if one needs to perform a more complex mutation, by changing multiple properties of a given node, for instance, that would cause subscribers to "see" the intermediary states required to complete the mutation. With transactions, mutations can be performed atomically:

const store = new Store({ user: { age: 32 }})
store.subscribe(console.log)

store.state.user.age++
// subscribers are notified
store.state.user.name = "Diego"
// subscribers are notified

Node#transaction would allow multiple mutations to be applied to a given node in an atomic fashion, similar to DB transactions:

const store = new Store({ users: [{ age: 32 }]})
store.subscribe(console.log)

store.state.users[0].transaction(user => {
  user.age++
  user.name = "Diego"
  console.log(user)
  // => { name: "Diego", age: 33 }
})
// subscribers are notified only once

Mutations performed within the #transaction closure happen "atomically" and subscribers will perceive the final mutation as if it were a single one. Additionally, within the transaction closure, mutations are visible right away (as shown in the example above).

Nodes must have their path refreshed

A GIF is worth 10000 words...

2018-04-10 20 28 45

Basically, a node bound to path /array/:index must have its path refreshed when items removed/added to the array affect the path.

Example:

const todos = [
  { id: 1, ... }, // path = /todos/0
  { id: 2, ... }, // path = /todos/1
]

When removing todos[0], the path of the remaining todo, shoud no longer be /todos/1 but rather /todos/0.

Support seamless mutation promise resolution

It would be very convenient if mutations could take promises in addition to sync values, example:

store.users.push(fetch("app.com/users/123"))
store.todos = fetch("app.com/todos?limit=20")

If the mutation value is a Promise, arbor would then only apply the corresponding mutation upon promise resolution.

Failures can still be handled by the caller:

const userPromise = fetch("app.com/users/123").catch(console.log)
store.users.push(userPromise)

This is particularly useful when APIs reflect exactly the data structure expected by the front-end (BFF pattern).

Implement store adapters to connect to different data sources

This would allow a local store to connect with different remote data sources, such as a Rest API, GraphQL, or even another arbor store via a peer-to-peer connection.

Rest APIs

store.connect(new MyRestApiAdapter)

GraphQL

http://graphql.org/graphql-js/running-an-express-graphql-server

store.connect(new MyGraphQLServerAdapter)

Web Sockets

https://www.html5rocks.com/en/tutorials/websockets/basics

store.connect(new MyWebsocketChannel)

WebRTC

https://www.html5rocks.com/en/tutorials/webrtc/basics

store.connect(new MyWebRTCConnection)

Local Storage

store.connect(new LocalStorageAdapter)

Provide @prop decorator to bind instance variables to props attributes

Allow binding instance variables to prop fields. This could provide an even higher data abstraction level.

class App extends React.Component {

  @prop // binds this.users to this.props.users
  users

  @prop("todo_list") // binds this.todos to this.props.todo_list
  todos

  render() {
    // JSX...
  }
}

Extract MTree into its owne arbor-model package

MTree provides a new state tree engine (extending the default PTree) adding support to model abstractions for registered state tree path patterns.

This could be a separate, optional package since developers may choose to not use models (for simple apps it might not be needed...) thus, reducing the number of dependencies one would have to bundle within their app.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.