A quote goes here
Event driven RPC between a Swift backend and a Clojurescript front end. Influenced by the Xi-editor.
Note 1: API is subject to change as I experiment implementing it within my own application.
Note 2: also a WIP. I am writing the documentation before I finish publishing the code :p
Here are some resources to help familiarise yourself with Native Modules for React Native, and the Xi-editor architecture:
Following Xi's design decisions:
The front end renders a UI derived from the events (facts) it has received. The front end should never block, and is modelled to be eventually consistent.
This project assumes you have a React Native project targetting ios
and a tool for building Clojurescript assets (eg shadow-cljs).
Your project needs to be able to support native modules. If you are using Expo, you will need to eject to ExpoKit.
Please check this example project for reference.
Reax ships with no transitive dependencies. The front end optionally depends on integrant, though you can use any library for managing app state.
Add this dependency to your projects deps.edn
file:
{}
Add this dependency to your projects ios/Podfile
file:
pod 'RNReax', :git => 'https://github.com/wavejumper/RNReax', :tag => '1.0.0'
A Reax event looks like this:
["getUser" {:userId "yoko"}]
A Reax event can be dispatched from either Clojurescript or Swift, and are considered asynchronous.
Generally a dispatch event will originate from the front end (eg via user interaction) and the Reax Swift module will asynchronously perform the operation.
It is up to the end-user to define the semantics of the operation (eg if the event is idempotent). Reax simply provides the glue.
During transport, events are represented as JSON (conveniently, thanks to the codable protocols in Swift, and js/JSON
in JS). Ideally, a transit-json codable impl would be nicer for richer types.
The Swift type system defines the schema of available events and valid arguments an event has:
Anything Decodable
is a valid event id, so long as it also implements the ReaxEventRouter
protocol. An event id is generally represented as an enum encoded as a string.
enum MyEventRouter: String, Codable, ReaxEventRouter {
typealias Context = MyContext
typealias Result = MyResult
case getUser
func routeEvent() -> ((_ ctx: Context, _ from: Data) -> Either<Result, ReaxError>) {
switch self {
case .getUser:
return eventHandler(GetUserHandler.self)
}
}
}
In this example we have defined a single event, getUser
, which maps to the GetUserHandler
(defined below).
The signature for routeEvent
returns a closure, which then returns the result.
eventHandler
is a helper fn that returns a closure that decodes, and invokes the event handler. This fn also neatly handles any deserialization errors.
The router is succinct for most use cases. The router should only care about matching event id's to event handlers.
Event handlers are represented with the ReaxEventHandler
protocol. Similar to event ids, anything that implements the Decodable
protocol is a valid handler. A handler is generally represented as a struct.
struct GetUserHandler: Codable, ReaxMutation {
typealias Context = MyContext
typealias Result = MyResult
var userId: String
func invoke(ctx: Context) -> Either<Result, ReaxError> {
return Either.Left(ctx.pool.getUser(userId))
}
}
These are two typealias
arguments both ReaxEventRouter
and ReaxEventHandler
must provide to allow for customised context and results.
The Context
typealias allows the end user to define required components (eg, a database pool or some custom state) that will get passed to the handler's invoke
method.
Context
can be any data structure implementing the ReaxContext
protocol. They are generally structs.
struct MyContext: ReaxContext {
var pool: Database
var channel: ((_ ctx: Context, _ from: Data) -> Either<MyResult, ReaxError>)
func state() -> ReaxContextState {
return ReaxContextState.Started
}
}
The Result
typealias is the value the Reax module sends to the front end. It can be represented as anything Encodable
, and is generally a struct, or enum.
struct MyResult {
var name: String
var userId: String
}
In the case of handling errors, the ReaxError
enum is returned to the user.
Finally, the ReaxEventEmitter
class is what the front end interacts with. This class is a subclass of RCTEventEmitter
.
A skeloton Reax class looks like this:
@objc(Midi)
class Midi: ReaxEventEmitter {
var ctx = MidiContext()
@objc
func start() {
logger.info("Starting MIDI")
let deviceManager = DeviceManager()
let observations = deviceManager.initSubscriptions()
self.ctx = MidiContext(deviceManager: deviceManager, observations: observations)
}
@objc
func stop() {
// TODO: implement shutdown hooks for compoents
self.ctx = MidiContext()
}
@objc(dispatch:args:)
func dispatch(id: NSString, args: NSString) {
self.invoke(MyEventRouter.self, ctx: self.ctx, id: id as String, args: args as String)
}
}
This method gets called when the front end sends an event to the module. There is a public invoke
method on the ReaxEventEmitter
class which neatly handles all deserialization, serialization and responding.
The ReaxEventEmitter
class contains one useful method, channelFactory
for constructing 'channels' out of the modules concrete Result
type.
This can be used for dispatching results to the front end asynchronously, eg some event triggered by Key-Value Observing
A <ModuleName.m>
file will also have to be created, exposing the module to the front end:
#import "React/RCTBridgeModule.h"
#import "React/RCTEventEmitter.h"
@interface RCT_EXTERN_MODULE(Midi, RCTEventEmitter)
RCT_EXTERN_METHOD(start)
RCT_EXTERN_METHOD(stop)
RCT_EXTERN_METHOD(dispatch:(NSString *)id args:(NSString *)args)
@end
All ReaxEventEmitter
classes have to implement start
and stop
methods, which will be invoked from the front end when the user initializes the Reax module.
These two methods are how you manage the lifecycle of stateful Reax modules, eg setting up Context
.
This pattern means that both configuration and lifecycle management should come from a centralized location: your front-end (eg, via integrant).
If you need more custom dependency injection, you can always implement the RCTBridgeDelegate protocol
The reax.integrant
namespace provides integrant integration via the :reax/module
key.
Within your integrant config, you can define a Reax module like so:
(ns example
(:require [integrant.core :as ig]
[example.link :as link]
[reax.integrant])) ;; <-- require for :reax/module key
(defmethod ig/init-key :app/db [_ init-value]
(atom init-value))
(defn config []
{:app/db {}
:link/handler {:db (ig/ref :app/db)
:handler link/handler} ;; <--- implementation explained in next section
[:reax/module :reax/link] {:class-name "Link"
:event-listener (ig/ref :link/handler}})
It accepts three keys:
:class-name
: the name of the Reax class:result-handler
: a function that handles all Reax results:error-handler
: a function that handles all Reax errorsThe returned value from the integrant component will be a map containing a :dispatch
key.
This is how you send events to your Reax module:
(let [{:keys [dispatch]} reax-module]
(dispatch "getUser" {:userId "Yoko"}))
All serialization will be taken care of!
Generally you will want to react to incoming events (eg, by mutating some app state).
A good pattern is to implement an event handler init-key
that has a dependency on your :app/db
, or whatever other context it requires:
(ns example.link
(:require [integrant.core :as ig]))
(defn handler [db event]
(assoc db :my-event event))
(defmethod ig/init-key :app/handler [_ {:keys [db handler]}]
(fn [event]
(swap! db handler event)))
For neater dependency injection, it would be nice if the start
method had a way to pass in generic args to the component...
There is nothing in this codebase that ties it to just Clojurescript. Look to the :npm-module target in shadow-cljs and offer NPM package.
They would be good :)
If there is an easy way to template/automatically define the boilerplate-y <ModuleName.m>
RCT_EXTERN_MODULE
code that would be nice.
The result type was added as a way to avoid having the invoke
operation return meaningless voids everywhere.
To better represent the async nature of Reax, and if all mutations are generally something side-effectful, perhaps something like bow effects would be a good idea for richer types.
The ReaxError
enum isn't really extensible, and without higher kinded types, I'm not really sure of the best way to allow for better extensibility.
JSON transport makes for a convenient, and type safe API thanks to codable. RCTConvert allows for helper functions all accept a JSON value as input and map it to a native Objective-C type or class.
Investigate this as an alternative for performance reasons.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close