Research
Security News
Quasar RAT Disguised as an npm Package for Detecting Vulnerabilities in Ethereum Smart Contracts
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Hierarchical router library for single page applications.
Abyssa is a stateful, hierarchical client side router.
What does stateful mean? It means all states are not equal and abyssa knows how to go from one state to another efficiently.
Abyssa does only one thing: Routing.
Upon entering a state, it can be rendered using any technique: Direct DOM manipulation, client or server side templating, with the help of a binding library, etc.
A state can even be abstract and not render anything.
Abyssa can be used like a traditional stateless url -> callback router:
var show = { enter: articleEnter };
var edit = { enter: articleEditEnter };
Router({
article: State('articles/:id', show),
articleEdit: State('articles/:id/edit', edit)
})
.init();
Or we can leverage abyssa's state machine nature and nest states when it serves us:
var article = { enter: loadArticle };
var show = { enter: articleEnter, exit: articleExit };
var edit = { enter: articleEditEnter, exit: articleEditExit };
Router({
article: State('articles/:id', article, {
show: State('', show),
edit: State('edit', edit)
})
})
.init();
Now we can freely switch between viewing and editing an article without any pause because the article data is loaded in the parent state and can be shared in the child states.
What is the main advantage of stateful routers? Performance: Less redraws, less wasteful data loading, less wasteful setUp logic, etc.
When going from a state A to a state B, as far as a stateless router is concerned, everything has to be done from scratch even if the two states are closely related. Trying to optimize state transitions by hand is going to be awkward and lead to an explosion of custom state variables. On the other hand, abyssa make it simple to reason about what makes each state different and thus compute the minimum set of changes needed to transition from state A to state B.
Here, Abyssa will simply swap the red bit for the green bit. Why should everything be redrawn? It's slower and the software would lose all the state implicitly stored in the previous DOM.
Read this excellent blog post for more information: Make the most of your routes
Note: With the emergence of VDOM approaches, using abyssa as a stateful router has less of an impact, as VDOM diffing/patching will usually take care of good enough performances. Also, a component based view library can handle hierarchical data loading and caching.
Using abyssa as a commonJS/browserify module
npm install abyssa
...
var Router = require('abyssa').Router;
Using abyssa as a global
Use one of the provided prebuilt files in the target folder.
The transition from the state A1
to the state B
would consist of the following steps:
A1 exit -> PA exit -> B enter
Configure the router before its initialization. The available options are:
Initialize the router.
The router will immediately initiate a transition to, in order of priority:
Add a new root state to the router.
Returns the router to allow chaining.
The state Object is a simple POJO. See State
Request a programmatic, synchronous state change.
While you can change state programmatically, the more idiomatic way to do it is sometimes using anchor tags with the proper href.
Two notations are supported:
// Fully qualified state name
transitionTo('my.target.state', { id: 33, filter: 'desc' })
// Path and (optionally) query
transitionTo('target/33?filter=desc')
The acc
parameter can be used to specify an object that will be passed up then down every state involved in the transition.
It can be used to share information from a state with the subsequent states.
Attempt to navigate to 'stateName' with its previous params or
fallback to the defaultParams parameter if the state was never entered.
Compute a link that can be used in anchors' href attributes
from a state name and a list of params, a.k.a reverse routing.
Returns the previous state of the router or null if the router is still in its initial state.
Returns the current state of the router.
Returns the state object that was built with the given options Object or that has the given fullName String.
Returns undefined if the state doesn't exist.
Returns whether the router is executing its first transition.
Replaces the current state's params in the history with new params.
Note: replaceParams
only works with urlSync
= history
.
The state is NOT exited/re-entered. That means you must store this params state outside the router to know
what to render. This functionality is useful when some url changes shouldn't re-render the whole application, nor create a separate entry in the browser history. (ex: scroll position, active filters, whether a popup is visible)
Returns the diff between the current params and the previous ones
var diff = router.paramsDiff();
{
update: { // params staying but being updated
id: true
},
enter: { // params making an appearance
q: true
},
exit: { // params now gone
section: true
},
all: { // all param changes
id: true,
q: true,
section: true
}
}
The paramsDiff is also accessible from the current state.
All event handlers receive the current state and the old state as arguments (of type StateWithParams).
To remove the event handler, attach a null/undefined callback.
States are simple POJOs used to build the router and represent path segments of an url (indeed, the router only matches routes against states' paths).
A state can also own a list of query params: While all states will be able to read these params, isolated changes to these
will only trigger a transition up to the state owning them (it will be exited and re-entered). The same applies to dynamic query params.
How much you decompose your applications into states is completely up to you.
A state is really just an object with an uri
property. Optionally, the following properties can be specified:
enter
, exit
, update
, data
, children
.
The path segment this state owns. Can also contain a query string. Ex: uri: 'articles/:id?filter'
Specify a function that should be called when the state is entered.
The params are the dynamic params (path and query alike in one object) of the current url.
This is where you could render the data into the DOM or do some general work once for many child states.
Same as the enter function but called when the state is exited. This is where you could teardown any state or side effects introduced by the enter function, if needed.
The update callback is called when the router is moving to the same state as the current state, but with different path/query params.
Specifying an update callback can be seen as an optimization preventing doing wasteful work in exit/enter, e.g removing and adding the same DOM elements that were already present in the document before the state change.
var router = router({
people: State('people/:id', {
enter: function() {},
update: function() {},
exit: function() {},
})
}).init('people/33');
During init, enter
will be called.
Later, if the router transitions from 'people/33' to 'people/44', only update
will be called. If an update
callback wasn't specified,
exit
then enter
would have been called in succession.
Custom data properties can be specified declaratively when building the state.
A map of child names to states.
Given a state represented by the path "articles", with a child state named "item" represented by the dynamic path "id".
When the router is in the state "articles.item" with the id param equal to 33, the browser url is http://yourdomain/articles/33.
There are at least 3 ways to build such a router; It is advised to build the router centrally, even if the state definitions are
located in their own modules.
Using pojos
var router = Router({
articles: {
uri: 'articles',
children: {
item: {
uri: ':id'
}
}
}
}).init();
Or using the State
factory shorthand:
var router = Router({
articles: State('articles', {}, {
item: State(':id', {})
}).init();
Or using the imperative form:
var router = Router();
var articles = State('articles');
articles.children.item = State(':id');
router.addState(articles);
router.init();
A state represented by the path "articles" with a path-less child state named "show"
When the router is in the state "articles.show", the browser url is http://yourdomain/articles
var state = State('articles', {}, {
show: State('')
});
router.addState('articles', state);
Now the articles state also tells us it owns the query param named 'filter' in its state hierarchy.
This means that any isolated change to the filter query param (meaning the filter was added, removed or changed but the path remained the same) is going to make that state exit and re-enter so that it can process the new filter value. If you do not specify which state owns the query param, all states above the currently selected state are exited and reentered, which can be less efficient. Also, Enumerating the possible query strings is mandatory if you want these to appear when using reverse routing or name-based state changes.
var state = State('articles?filter', {}, {
show: State('')
});
Additionaly, the last path segment of a state can end with a *
to match any number of extra path segments:
State('path/:rest*')
// All these state changes will result in that state being entered:
// router.transitionTo('path'); // params.rest === undefined
// router.transitionTo('path/other'); // params.rest === 'other'
// router.transitionTo('path/other/yetAnother'); // params.rest === 'other/yetAnother'
StateWithParams
objects are returned from router.previous()
, router.current()
and passed in event handlers.
The current uri associated with this state
The path and query params set for this state
The (local) name of the state
The fully qualified, unique name of the state
Returns whether this state or any of its parents has the given fullName.
By default, the router will intercept anchor clicks and automatically navigate to a state if some conditions are met (left button pressed, href on the same domain, etc).
This behavior can be turned off by using the corresponding router configuration setting
You may want to turn off anchor interception on mobile optimised apps and perform manual router.transitionTo() calls on touch/pointer events.
You can also intercept mousedown events instead of the usual click events by using a data-attribute as follow:
<a data-nav="mousedown" href="/">
If a same-domain link should not be intercepted by Abyssa, you can use:
<a data-nav="ignore" href="/">
Demo: Abyssa demo async
Source: Abyssa demo async source
States must be added to the router but states also often need to call methods on the router, for instance to create href links.
This creates circular dependencies which are annoying when using primitive module systems such as CommonJS'.
To break that circular dependency, simply require the api object instead of the router in your states:
var api = require('abyssa').api;
// then api.link('state', { id: 123 })
It is much easier to reason about an application and its routes if the various uris can be all be read in one place instead of being spread all over the code base. However, states should be modularized for the sake of easier maintenance and separation of concerns. Here's how it might be achieved with CommonJS modules:
// router.js
var Router = require('abyssa').Router;
var State = require('abyssa').State;
var index = require('./index'),
articles = require('./articles'),
articlesDetail = require('./articles/detail'),
articlesDetailEdit = require('./articles/detailEdit');
Router({
index: State('', index),
articles: State('articles', articles, {
articlesDetail: State(':id/show', articlesDetail),
articlesDetailEdit: State(':id/edit', articlesDetailEdit),
})
}).init();
// index.js
module.exports = {
enter: function() {
console.log('index entered');
},
exit: function() {
console.log('index exited');
}
};
update
update
is an optional hook that will be called whenever the router moves to the same state but with updated path/query params.
However, not all params are equal: A change in the path param representing the resource id may induce more work than the change of some secondary query param.
Example of a conditional update:
var api = require('abyssa').api;
var state = State({
enter: function(params) {
loadResourceForId(params.id);
},
update: function(params) {
var diff = api.paramsDiff();
// The id was changed
if (diff.update.id) {
loadResourceForId(params.id);
}
// Some other params were changed
else {
filterInPlace(params);
}
}
});
Check this gist to get a ReactState
that can be used to automatically insert React children based on routing:
FAQs
Hierarchical router for single page applications
We found that abyssa demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket researchers uncover a malicious npm package posing as a tool for detecting vulnerabilities in Etherium smart contracts.
Security News
Research
A supply chain attack on Rspack's npm packages injected cryptomining malware, potentially impacting thousands of developers.
Research
Security News
Socket researchers discovered a malware campaign on npm delivering the Skuld infostealer via typosquatted packages, exposing sensitive data.