clearwater-rb / grand_central Goto Github PK
View Code? Open in Web Editor NEWState-management and action-dispatching for Ruby apps
State-management and action-dispatching for Ruby apps
Right now, all actions are handled synchronously. I've been thinking about ways to handle async actions for a while and @wied03 asked me about them, so I figured I'd open an issue to discuss them a bit more formally.
The idea I had was to add a special AsyncAction
class so you could do something like this:
# The AsyncAction would signal the store to add then/fail handlers
FetchThing = GrandCentral::AsyncAction.with_attributes(:id) do
# The promise method is what the then/fail handlers would
def promise
Browser::HTTP.get("/api/things/#{id}")
end
end
# Typical synchronous LoadX action: convert a hash (i.e. from JSON) to the right kind of object
LoadThing = GrandCentral::Action.with_attributes(:hash) do
def thing
Thing.new(hash)
end
end
store.dispatch FetchThing.new(thing_id) do |response|
store.dispatch LoadThing.new(response.json[:thing])
end
Inspired by Redux's middleware implementation, I think it would be pretty powerful to allow a Store to take a list of middleware to wrap around the reducer block.
I propose adding the following to the Store initializer:
module GrandCentral
class Store
def initialize initial_state, middlewares = [], &reducer
@state = initial_state
@reducer = middlewares.inject(reducer) do |stack, middleware|
middleware.new(stack, self)
end
@dispatch_callbacks = []
end
end
end
That would allow you to do something like this:
APIRequest = Action.with_attributes :endpoint
APISuccess = Action.with_attributes :data
APIFailure = Action.with_attributes :error
class APIFetcher
def initialize(reducer, store)
@reducer = reducer
@store = store
end
def call(state, action)
fetch(action.endpoint) if APIRequest === action
@reducer.call(state, action)
end
def fetch(url)
Browser::HTTP.get(url).then { |data|
@store.dispatch(APISuccess.new(data))
}.fail { |error|
@store.dispatch(APIFailure.new(error))
}
end
end
class DispatchLogger
def initialize(reducer, store)
@reducer = reducer
@store = store
end
def call(state, action)
puts action.class.name
@reducer.call(state, action)
end
end
Store = GrandCentral::Store.new(initial_state, [APIFetcher, DispatchLogger]) do |state, action|
case action
when APIRequest
# Do something here if you want...
when APISuccess
state.merge(data: action.data)
when APIFailure
state.merge(error: action.error)
else
state
end
end
Thoughts?
Would be very useful if there was an out of the box connector to Redux DevTools extension. Integrated Time travel debugging would be awesome.
Given "redo" is a control statement, would it make sense to rename VersionedStore#redo
to VersionedStore#rollforward
? That way it's similar to "rollback". Or perhaps VersionedStore#rewind
and VersionedStore#replay
?
Just an idea I'm tossing out there. I discovered grand_central recently and gave it a whirl and that caught me by surprise.
Given that current state is a reduction of the list of all actions performed on the initial state, making actions serializable could allow a store to send bug reports automatically. To do so, it could hold onto the initial state and action history, and when an exception is raised, it could be told to send a report to Bugsnag or another error-tracking service or even back to the app's home server.
Serializing actions would also make it possible to run the store and an entire Clearwater app on a web worker. The worker could take serialized actions from the UI thread using postMessage
(which is not exposed in Bowser … yet), then the Clearwater app would generate the new vdom tree, diff against the previous tree, then pass the resulting patch back to the UI thread to make the changes to the DOM.
To make this possible, we would need to be able to convert arbitrary subclasses of GrandCentral::Action
and GrandCentral::Model
to and from native JS objects. In Opal, the conventions for this are Klass#to_n
("to native") and Klass.from_native(js_object)
.
For both classes, we control object initialization, so we know what gets passed in and what it's called. For example, with the following action:
Foo = GrandCentral::Action.with_attributes(:foo, :bar)
Then Foo.new(123, 'abc')
can be serialized into { foo: 123, bar: "abc" }
. We would have to have some additional metadata to be able to turn it back into a Foo
action, though. Maybe { foo: 123, bar: "abc", __grand_central: { class: "Foo" } }
? Once it's in the form of a native JS object, it's easily stringified to be able to send across the wire.
Same goes for a GrandCentral::Model
:
class Bar < Model
attributes(:id, :baz, :quux)
end
Bar.new(
id: 123,
baz: "abc",
quux: "xyz",
).to_n
# {
# id: 123,
# baz: "abc",
# quux: "xyz",
# __grand_central: {
# class: "Bar",
# },
# }
I don't know what the CPU-time cost of all this is, but especially across a web worker it's probably significant; serializing large object graphs would mean recreating them on the other end (workers don't share memory with the UI thread). Either way, it'd be a good idea to measure.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.