Skip to content

rijs/docs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ripple is a set of simple modules that compose to form a modular framework. This means you can more easily:

  • Extend: Simply bolt-on a new module, without having to migrate to a whole new framework
  • Upgrade: Keep up with the diverse and fast-moving world of the Web/JavaScript
  • Control: Stay on top of how much you want to customise or how close you want to keep to VanillaJS
  • Adapt: Many projects (e.g. legacy/enterprise) have different stringent requirements which may evolve
  • Future-Proof: Avoid lock-in with more control over the framework layer and disposable modules
  • Experiment: Swap out a module for a different or more performant one
  • Automate: Abstract your repeated patterns into the framework layer to focus on business logic
  • Integrate: With a better separation of concerns, write focused adaptors to connect to your infrastructure

It starts off with a super-trivial, but extensible, core module. This simply gets/sets things (resources), and emits a change event when something is updated:

var ripple = core()

ripple(name)       // getter
ripple(name, body) // setter
ripple.on('change', function(name[, change]){ .. })

All of the other modules build on this simple module to layer in new behaviours and affordances. Read more about how it works end-to-end in the primer.


## Links
## Official Distributions

You will most likely want to compose your own ripple.js in major apps, but the two common out-of-the-box builds are:

For the client, there are four flavours of ripple you can use depending on how you plan to package your application:

  • ripple.js - all-in-one, unminified
  • ripple.min.js - all-in-one, minified
  • ripple.pure.js - does not include external dependencies (socket.io and utilise), unminified
  • ripple.pure.min.js - does not include external dependencies (socket.io and utilise), minified

These are all also automatically exposed as endpoints on your server (in rijs/fullstack). Bundling is done with Browserify and UglifyJS. If you wish to produce your own bundle, you can start by taking a look at the npm run build command.


## Index of Modules
  • Core: - A simple extensible in-memory data structure of resources.

  • Data (type): Extends core to register objects and arrays. It also enables per-resource change listeners on those resources, as well versioning info.

  • Functions (type): Extends core to register functions. For cases when a function resource is registered as a string (e.g. from WS, localStorage), this converts it into a real function before storing.

  • Hypermedia (type): - Extends core to register a HATEOAS API as a resource, traverse links to other resources, and cache them.

  • Sync: Synchronises resources between server and client whenever they change. You can specify transformation functions on incoming and outgoing updates to control the flow of data on a per resource, per resource type, or global level. See the Primer#Sync for more info.

  • Backpressure: By default, the Sync module sends all resources to all clients. Users can limit this or change the representation sent to clients for each resource using the to transformation function. This module alters the default behaviour to only send resources and updates to clients for resources that they are using to eliminate over-fetching. You can still use the to hook to add extra business logic on top of this.

  • Components: Redraws any custom elements on the page when any of it's dependencies change (either the component definition, data, or styles).

  • Features (render middleware): Extends the rendering pipeline to enhance a component with other features (mixins).

  • Needs (render middleware): Extends the rendering pipeline to apply default attributes defined for a component.

  • PreCSS (render middleware): Extends the rendering pipeline to prepend stylesheet(s) for a component. It will be added to either the start of the shadow root if one exists, or scoped and added once in the head.

  • Shadow DOM (render middleware): Extends the rendering pipeline to append a shadow root before rendering a custom element. If the browser does not support shadow roots, it sets the host/shadowRoot pointers so that a component implementation depending on them works both in the context of a shadow root or without.

  • Delay (render middleware): Extends the rendering pipeline to delay rendering a view by a specified time (ms)

  • Perf (render middleware): Extends the rendering pipeline to log out time taken for render of every component to help with fine-tuning by highlighting performance bottlenecks. Should only be used in development.

  • DB: Allows connecting a node to external services. For example, when a resource changes, it could update a database, synchronise with other instances over AMQP, or pump to Redis.

  • MySQL (adaptor): Registers a new database adaptor to interface with a MySQL DB.

  • Offline: Loads resources from localStorage on startup (which has massive impact on how fast your application is perceived) - as opposed to waiting for subsequent network events to render things. Asynchronously (debounced) caches resources when they change.

  • Helpers: Allows registering helper functions for a resource.

  • Sessions: Enriches each socket with a uniquely identifying matching sessionID.

  • Auto Serve Client: Exposes the distribution files as endpoints on your server.

  • Singleton: Exposes the instance globally on (window || global).ripple.

  • Versioned: - Adds global versioning info and enables rolling back individual resources or the entire application.

  • Server Side Rendering: This registers a middleware on your server to expand any Custom Elements using the same components logic and available resources at the time before sending the page to the client.

  • Resources Directory: Loads everything in your ./resources directory on startup so you do not have to require and register each file manually. During development, this will watch for any changes in your resources folder and reregister it on change. So if you change a resource it will be synchronised with the client, then redrawn without any refreshes (hot reload). See rijs/export for similar if you are using Ripple without a server.

  • Export: Combines all resources under the ./resources directory into a single index.js file. This is so you can export and import a bundle of resources from separate repos by simplying requireing them. Note that this is not the same bundling, since function resources are linked with requires, such that you could subsequently browserify index.js to produce a client bundle.

  • Deprecated - Pre-apply HTML Template: In case you like to pre-apply HTML templates before operating on it with JS. I've long moved away from this, but kept it as it serves an example of how modules for other templating types could be supported.

  • Deprecated - Reactive: Watches any data resource for changes and emits a change event when it does, to avoid the repetitive boilerplate in manually dispatching events. Uses Object.observe, or fallback to polling.


## Comparisons

Flux

Ripple shares some architectural concepts with Flux, such as the single dispatcher, unidirectional data-flow programming, and views updating when the associated data changes, etc. However, Ripple is an extension to the Flux paradigm, in that the dispatcher will not update only the data/views on the current page, but on all other clients too. Ripple introduces much less proprietary concepts (everything is a resource) and the API aims to embrace standards (e.g. Custom Elements) rather than invent new ones.

Meteor

Ripple and Meteor share some key benefits, such as reactive programming, hot code push, database everywhere, etc. The key difference however is that Ripple is a library whereas Meteor is a framework - one that deeply takes over your entire development fabric and locks you in. There is no inversion of control with Ripple and you can use it many different ways. Besides that, the implementation of the aforementioned benefits is more powerful in Ripple: For example, not being tied to a particular templating engine (Spacebars) and the Meteor 'database everywhere' is just a 'prototyping concept' removed for production apps requiring additional wiring that defeats the original friction-reducing purpose of having it. In contrast, Ripple's generic proxies allows you to filter out 'privacy-sensitive data' and the overall architecture gives you 'latency compensation' for free (changes to data update dependent views immediately, and if the change was unsuccessful there will be another invocation to put it in the correct state).

Compoxure

Ripple and Compoxure are very similar in decomposing applications in terms of independent resources. Like Compoxure, Ripple can call out for resources from separate micro-services, avoiding the monolith criticism (1), it can pre-render views avoiding the SEO-incompatibility criticism (2) and it's doesn't lock you in to using a particular hybrid-approach like React or rendr (i.e. you could call a Java service to generate the resource) (3). The key difference is that whereas Compoxure is only concerned with the first render and requires manually wiring up event listeners and AJAX calls for updates, resources in Ripple are long-lived and continue to send/receive updates after the first render. Ripple does not currently cache resources in Redis (but that's on the roadmap).

Basket

Ripple and Basket both use localStorage for storing and loading from. Basket does this on a script-level however, whereas Ripple does this on a resource-level. Basket uses localStorage as an alternative to the browser cache, whereas Ripple uses it for the initial page render and then re-renders relevant parts when there is new information available streamed from the server.

Polymer

Ripple and Polymer both embrace Web Components for composing applications, but beyond auto-generating Shadow DOM roots for upgraded Custom Elements and transparently encapsulating styles for non-Shadow DOM users, Ripple does not provide anywhere near the same level of sugar as Polymer on top of Web Components.

Redux

The Ripple Core is very comparable to Redux as a single container for application state that you can subscribe to for updates. Ripple Minimal (core + components module) is architecturally analagous to redux + react. However, core is lighter and more simple that redux in that it only "sets", whereas redux is structured around a reducer API (and other boilerplate). It would be trivial to write a helper function on top of core that feeds the current state and the action to a function that then returns the result to update with.

loadCSS

The Ripple architecture results in the same behaviour as loadCSS offers. CSS modules (in fact all resources) are loaded asynchronously (streamed) and appended in order to preserve cascading. If you are using Shadow DOM, they will be added at the start of the shadow root and to the head of the document if not (see PreCSS for more info). In addition (i) only the modules you are currently using will be appended (ii) this is not something you need to manually manage at all but is inferred from a declarative syntax.

Cycle

There is a single "change" event in Ripple that hooks into many different focused modules ("drivers"). This leads to a similar pattern with Cycle where you have a declarative flow of data throughout the application and this nervous system may be even more extensive as it creates pipelines across processes, network, and services. The key difference is that whereas Cycle is based on high-level RxJS as it's primitive, Ripple uses a simple stream of events as it's low-level primitive. Promises, observables, streams in all their incarnations can be modelled from individual events. This leads to a leaner core and allows users to opt-in using RxJS in their applications or modules. It also has major implications for your application size (minified todo app > 1MB) and performance.

GraphQL

GraphQL is about co-locating data requirements of a component with the component itself, which has always been a part of Ripple (<component data="name">). The key difference is that whereas GraphQL deals with making queries by specifying the shape of the response, Ripple prefers "named resources", whose representation on the client is determined by a declarative transformation function. In this respect, Ripple is more generic since you could build a GraphQL transformation function whose response depends on the incoming query. Furthermore, the data-fetching in Ripple is inverted (push vs pull), so everything is realtime by default without unnecessary plubming, and everything happens over WS rather than HTTP, which avoids expensive round-trips due to headers.

ImmutableJS, Mori

ImmutableJS uses HAMT data structures to efficiently store and update data. This is useful because you can use cheap === checks to skip drawing a component if the data has not changed. However, the downside is that it has an expensive startup cost (~3x clone with JSON!). This is non-trivial if you consider how much data crosses the HTTP/WS/localStorage/Worker boundary. In general Ripple is agnostic to whatever data you register. However, as an alternative solution to the shouldComponentUpdate problem, Ripple automatically tracks a log of fine-grained changes made on each local resource so the log.length can be used as revision counter. Beyond the UI and one VM in particular, the structural sharing of hash map tries doesn't help us with the efficient and reliable replication of data across nodes either, which is where the fundamental log/events structure is very useful.


## Contributing

The best way to currently contribute to Ripple is by creating your own module. Modules (and all extensions below) are simply decorator functions that take in an existing instance, set up new behaviour and then return the instance:

// alerts every change:
export default function alerter(ripple) {
  ripple.on('change', name => alert(name, 'changed!'))
  return ripple
}

// instantiate:
const ripple = alerter(core())

Besides listening for and acting on changes, there are a few other key extension points:

ripple.draw

This function is introduced by the components module and on change, simply redraws a component on an element using component.call(<el>, data). There are number of modules that extend this (marked with "render middleware" above). As an example, the following logs out a message everytime a component is drawn before continuing with the rest of the rendering pipeline:

export default function logRenders(ripple) {
  ripple.render = render(ripple.render)
  return ripple
}

const render = next => el => (log('drawing', el), next(el))

to | from

These two transformation functions are introduced by the sync module and on change applies to before emitting the change or applies from on incoming changes. There are actually three levels for both:

export default { 
  name: 'users'
, body: []
, headers: { from, to }
}

// simply reject all attempted changes
function from(){ return false } 

// only send the number of users, not the entire array
function to({ body }){ return body.length }

It's best to just clone an existing module so you can get all the boilerplate things like babel, builds, tests, coverage, etc out of the way.

ripple.types

You can create new types by registering the header under ripple.types. This primarily allows you to parse a resource that a user attempts to register before storing it. For example, with the hypermedia module, if a URL is registered, that URL is requested and the response is stored rather than then URL string:

ripple('github', 'https://api.github.com')
// ripple('github') would then return the root object with all the links

Each type can also have a render method which is what the components module uses to know how to render a resource. This is why data resources redraw all [data~=name] but functions redraw all component-name. This method will be important if you want to create your own templating types (e.g. text/handlebars).

ripple.adaptors

This collection is introduced by the db module, which standardises the way a ripple node connects to external services by taking in a connection string (type://user:password@host:port/database), destructuring it, and invoking the corresponding ripple.adaptors[type] with the object. The result (object of functions for each change type { add, update, remove }) is stored in ripple.connections which will be invoked when the corresponding change type happens. See MySQL as an example which translates atomic changes into SQL statements and executes them.


## API

This is an overview of the API categorised by each module that introduces it

Instantiation

# ripple = require('rijs')(opts)

opts is optional and will be passed to each module which may need different things from it

# ripple(name)

return the named resource, creating one if it doesn't exist

# ripple(name, body)

create or overwrite the named resource with the specified body

# ripple(name, body, headers)

create or overwrite the named resource with the specified body and extra metadata

# ripple({ name, body, headers })

create or overwrite the named resource with the specified body and extra metadata

# ripple([ .. ])

register multiple resources

# ripple(ripple2)

import resources from another ripple node

# ripple.resource(name[, body[, headers]])

alias for ripple as above that allows method chaining for registering multiple resources

# ripple.on('change', function(name, change))

react to all changes. change info, if available, is tuple of { key, value, type, time }

# ripple.once('change', function(name, change))

react once to a change. change info, if available, is tuple of { key, value, type, time }

# ripple.emit('change', [name[, change]])

emit a change for resource name and optional change info

# ripple.types

collection of all registered types

# ripple.resources

collection of all registered resources

# [header]content-type

resource type, usually interpreted based on the body type so not necessary to explicitly set this value

# ripple(name).on('change', function(change))

react to changes on the named resource (function receives name and change object, if any)

# ripple(name).once('change', function(change))

react once to a change on the named resource

# ripple(name).emit('change', [change])

emit a change for the named resource with optional change info

# [header]log

the max length for the changelog for this resource, negative values produce hollow log.

# require('rijs')({ server })

server is the http.Server instance (express app) to connect to clients on

# ripple.io

the underlying socket.io instance

# ripple.stream(sockets)(resources)

emit all or some resources, to all or some clients.

values:

  • sockets can be nothing (means send to all sockets), a socket, or a sessionID string
  • resources can be nothing (means send all resources) or name of resource to sync

# ripple.{to | from}({ name, body, headers }, { key, value, type, type })

transformation function to apply on all outgoing/incoming changes, default: identity

# ripple.types[type].{to | from}({ name, body, headers }, { key, value, type, type })

transformation function to apply on outgoing/incoming changes of each type, default: identity

# [header][to | from]

transformation function to apply on outgoing/incoming changes of specific resource, default: identity

# ripple.deps(el)

returns an array of the dependencies of an element

# ripple.draw()

redraw all components on page

# ripple.draw(element)

redraw specific element

# ripple.draw.call(element)

redraw specific element

# ripple.draw(name)

redraw elements that depend on named resource

# MutationObserver(ripple.draw)

redraws element being observed

# ripple.render(element)

the underlying function that actually renders the element

# <component-name>

invokes the component specified by nodeName on the element

# [attr][data]

passes specified data resources to component on render

# [attr][inert]

always ignore rendering this element

render middleware

# [attr][is]

invokes specified mixin(s) in same manner as original component

render middleware

# [header]needs

blocks rendering if any specified attributes missing, adds them and retries

render middleware

# [attr][css]

adds specified stylesheets to component

render middleware

# [attr][delay]

delays rendering of component by specified time

# require('rijs')({ db: 'type://user:password@host:port/database' })

connects ripple to something else, synchronishing changes. type must exist in ripple.adaptors. can be array of strings.

# ripple.adaptors

collection of services ripple knows how to connect to. mysql only by default. each new adaptor must be a function that takes the destructured connection string { type, user, password, host, port, database } and returns an object with functions { add, update, remove } to be called when the corresponding event occurs.

# ripple.connections

list of all active connections to external services

adaptor

# [header]mysql.table

specify which database table/collection to populate the resource with and sync changes with

# [header]mysql.to

transformation function to apply on outgoing db changes

# [header]cache

caching behaviouring for a resource. values: no-store | undefined (default)

# [header]helpers

collection of helper functions to make accessible on resource

# require('rijs')({ name })

name of the session ID cookie

# require('rijs')({ secret })

secret used to sign session ID cookie

see distributions for list of endpoints this exposes

# (window | global).ripple

makes ripple instance globally accessible

# ripple.version(name)

retrieves the current version index for the named resource

# ripple.version(name, i)

rollbacks the named resource to version i and returns its value at that time

# ripple.version()

retrieves the current historical index for the entire application

# ripple.version(i)

rollbacks entire application state to version i

Releases

No releases published

Packages

No packages published