Giter Site home page Giter Site logo

wildhoney / snapshot.js Goto Github PK

View Code? Open in Web Editor NEW
202.0 14.0 12.0 1.2 MB

Node.js app for slicing and dicing ~100,000 models in <5ms with easy sorting, paginating, and filtering.

Home Page: http://node-snapshot.herokuapp.com/

JavaScript 84.35% CSS 0.57% Ruby 5.41% HTML 7.60% Gherkin 2.07%

snapshot.js's Introduction

Snapshot.js

Node.js app for slicing and dicing paginated chunks of data with easy sorting and filtering.

Travis CI

 

NPM Version

Demo: http://snapshot.wildhoney.io/

Installing via npm

Snapshot is added to the npm registry, and can therefore be downloaded with npm install node-snapshot.

Dependencies

All dependencies can be installed with bower install and npm install, however the list is as follows:

Quick Start

Once a WebSocket connection has been successfully established, you're able to bootstrap Snapshot, passing in the WebSocket (Socket.IO) reference as a dependency.

var $snapshot = new Snapshot().bootstrap(socket).useDelta(false);

You then need to tell Snapshot which collection it's going to be creating snapshots of. You can pass in an optional string for the primaryKey – if you omit the primaryKey then the first key of the first model is used.

$snapshot.setCollection(collection, primaryKey);

(primaryKey is an optional parameter.)

Snapshot listens for three events natively, and these are handled automatically.

  • snapshot/:namespace/perPage – Set amount per page;
  • snapshot/:namespace/pageNumber – Set current page number;
  • snapshot/:namespace/sortBy – Set order column and ascending/descending;

As Snapshot supports multiple instances, a namespace is used to distinguish the events. If you don't explicitly specify a namespace in the instantiation then it will be default. Therefore all of your events will be: snapshot/default/perPage, snapshot/default/pageNumber and snapshot/default/sortBy.

(:namespace is the name you provided upon instantiation of Snapshot – if you didn't, then it's default.)

When the collection has been updated, Snapshot emits the snapshot/:namespace/contentUpdated event, passing through the snapshot as the first argument, and statistics relating to the request as the second argument.

For sorting by any given column, you can emit the snapshot/:namespace/sortBy event passing in the key and direction (ascending/descending). If you omit the direction property (or set its value to false) then Snapshot will cleverly invert the current sorting direction for you.

socket.emit('snapshot/:namespace/sortBy', property, 'descending');

Filtering

In addition to sorting and limiting, Snapshot also allows for the filtering of the collection. For this you can use the applyFilter method. Unfortunately you will need to read Crossfilter's API Reference before you begin filtering – or for simple filtering you can use Snapshot's primitive bundled filters.

socket.emit('filterByWord', text);

You can apply a filter however you like. It doesn't necessarily need to be applied via WebSockets, you could just as well use vanilla Node.js or Express.js. In our example though, we emit the filterByWord event to the Node.js server, and then we need to listen for that event.

socket.on('filterByWord', function(text) {

    $snapshot.applyFilter('word', function(dimension) {

        dimension.filterFunction(function(d) {
            var regExp = new RegExp(text, 'i');
            return d.match(regExp);
        });

    });

});

You essentially invoke the applyFilter on the snapshot object. Snapshot will pass in the dimension argument to your lambda function – this context is preserved. It's then entirely up to you to apply that dimension to the collection.

If you would like to clear a specific dimension, then you can use the clearFilter method – which takes the property name as its one and only argument.

$snapshot.clearFilter('word');

You can also clear every single filter by using the clearFilters method.

$snapshot.clearFilters();

Sometimes you may wish to clear certain filters before applying other filters, in which case you should utilise the composite filters approach by passing through the dimensions you wish to clear as well. You can then invoke the filterAll method on the dimension(s) you wish to clear.

$snapshot.applyFilter(['latitude', 'longitude'], function(latitudeDimension, longitudeDimension) {

    // Clear the latitude dimension.
    latitudeDimension.filterAll();

    // But then apply the longitude dimension.
    longitudeDimension.filterFunction(function(d) {
        return (d > 0);
    });

}, 'reduce');

Composite Filters

Before the release of 0.2.7 you could only apply filters one-at-a-time, which meant if you grouped filters together then the contentUpdated event would be emitted for each filter.

However, 0.2.7 introduced the concept of composite filters, which are nothing more than groups of filters which can be applied together, and have the event emitted once all filters have been applied. In order to create a composite filter, simply pass in an array of properties to filter on to the applyFilter method – the callback in the second argument will be the dimension of each property.

$snapshot.applyFilter(['latitude', 'longitude'], function(latitudeDimension, longitudeDimension) {

    // Filter to ensure the latitude is above 50, and longitude above 0.

    latitudeDimension.filterFunction(function(d) {
        return (d > 50);
    });

    longitudeDimension.filterFunction(function(d) {
        return (d > 0);
    });

    // Event will now be fired to reflect the current collection state.

}, 'reduce');

Bundled Filters

In light of Crossfilter's learning curve, Snapshot ships with a handful of bundled filters for common filtering techniques. These can all be invoked by emitting an event with a corresponding value.

  • snapshot/:namespace/fuzzyFilter {String}
  • snapshot/:namespace/exactFilter {String}
  • snapshot/:namespace/rangeFilter {Array}
  • snapshot/:namespace/regExpFilter {String} {String}
  • snapshot/:namespace/inArrayFilter {String} {Array}
  • snapshot/:namespace/notInArrayFilter {String} {Array}
  • snapshot/:namespace/clearFilter {String}
socket.emit('snapshot/default/fuzzyFilter', 'word', 'abc');
socket.emit('snapshot/default/exactFilter', 'word', 'kittens');
socket.emit('snapshot/default/rangeFilter', 'word', [12, Infinity]);
socket.emit('snapshot/default/regExpFilter', 'word', '[a-z0-9]+', 'ig');
socket.emit('snapshot/default/inArrayFilter', 'word', ['firstWord', 'secondWord']);
socket.emit('snapshot/default/notInArrayFilter', 'word', ['thirdWord']);
socket.emit('snapshot/default/clearFilter', 'word');

Each bundled filter expects the event name (snapshot/default/fuzzyFilter), the key (word), and value (abc).

Filtering Types

By default when you apply a filter, the previous filter will be cleared which is mostly likely the behaviour you're looking for. However, what if the user clicks red, and then clicks blue? Wouldn't it be nice if we could filter by both red and blue? In that case you're looking for the third argument of the applyFilter method.

$snapshot.applyFilter('word', ..., 'reduce');
  • afresh – filtering cleared before each filter;
  • reduce – filtering applied on current collection;

Ranges

Since the browser does not download every model into the browser, it's impossible to determine what the minimum/maximum for any given key is without loading all models. Snapshot therefore allows you to specify which columns you wish to generate minimum/maximum ranges for.

Please be careful with these as too many may noticeably slow down your Snapshots.

The following would specify that you wish to retrieve the ranges for the id property on every content change.

$snapshot.setRanges(['id']);

When the content changes you can access the range with stats.ranges.id.min and stats.ranges.id.max.

Multiple Instances

When instantiating Snapshot you should pass in the namespace for the current collection – that way you could create a new instance of Snapshot with a unique collection of models.

var $dogs       = new Snapshot('dogs').bootstrap(socket).useDelta(false);
var $cats       = new Snapshot('cats').bootstrap(socket).useDelta(false);
var $rabbits    = new Snapshot('rabbits').bootstrap(socket).useDelta(false);

In the above example Snapshot will have 9 events to listen to (3 events * 3 snapshots):

  • snapshot/dogs/perPage, snapshot/dogs/pageNumber, snapshot/dogs/sortBy
  • snapshot/cats/perPage, snapshot/cats/pageNumber, snapshot/cats/sortBy
  • snapshot/rabbits/perPage, snapshot/rabbits/pageNumber, snapshot/rabbits/sortBy

And it will emit 3 events:

  • snapshot/dogs/contentUpdated
  • snapshot/cats/contentUpdated
  • snapshot/rabbits/contentUpdated

If you don't create a namespace then the namespace will be set to default.

Delta Updates

There may be instances where sending delta updates is preferable to re-sending whole models. Snapshot supports the providing of delta updates – essentially, any models that have already been transmitted across the wire will not be sent again in their entirety; instead only their primary ID is sent.

var $snapshot = new Snapshot().bootstrap(socket).useDelta(true);

Once you've enabled delta updates using useDelta(true) as part of the bootstrap process, Snapshot will keep a history of transmitted models. It's crucial that you set the appropriate primary ID when invoking setCollection, otherwise a default primary key will be assumed.

$snapshot.setCollection([{ id: 1 }, { id: 2 }, { id: 3 }], 'id');

Note: You can suppress the initial event when invoking setCollection by passing true as the third argument.

Since unique models will only ever be transmitted once, it's imperative that you keep a history of all models from the snapshot/:namespace/contentUpdated event, and then to utilise those from your local cache when you come across a delta model.

Delta models are nothing more than the primary key of the model, which will help you lookup the model from your own collection cache. Therefore to detect a delta model, simply use something like Number.isFinite (or Underscore's _.isNumber) on the returned collection.

socket.on('snapshot/:namespace/contentUpdated', function(models, stats) {

    _.forEach(models, function(model) {

        if (_.isNumber(model)) {
            // Delta model!
            return;
        }

        // ...

    });

});

Example

Snapshot comes bundled with an example to get you started.

  • Navigate to example/server and run node default.js;
  • Open example/client/index.html in your browser;

Architecture

Below is a simple diagram of how Snapshot works. It demonstrates how the snapshot/:namespace/pageNumber event operates – which is also the same way other native Snapshot events function. It also demonstrates the flow of custom filters.

Snapshot Architecture

  • Browser establishes a WebSocket connection to Node.js – models are added;
  • Browser emits snapshot/:namespace/pageNumber event with data (example);
  • Snapshot along with Crossfilter updates the collection snapshot;
  • Snapshot emits snapshot/:namespace/contentUpdated event with the updated collection;
  • Browser emits a custom event (customFilterApplied) with the data;
  • Node.js listens for the customFilterApplied event and then interacts with Snapshot;
  • Snapshot emits the snapshot/:namespace/contentUpdated event with the updated filter applied;

Testing

Unit Testing

Grunt is a prerequisite to run the Mocha tests, which is installed when you run npm install. Afterwards all of Snapshot's unit tests can be run with the grunt test command from the terminal.

Cucumber

Snapshot also comes bundled with a handful of Cucumber tests.

  • cd tests/cucumber
  • bundle install
  • bundle exec cucumber

Philosophy

Loading a large collection of models into the browser is slow and unnecessary, instead Snapshot uses WebSockets to serve snapshots of those models to the browser when requested. It retains the state of the models, and so if a filter is changed, or the page number incremented, it will modify the snapshot only for that client.

Snapshot is also tremendously fast because of its use of Socket.io and Crossfilter. Snapshot listens for events to change the state of the collection of models, and fires another event to let the client know the snapshot was updated. Crossfilter allows Snapshot to quickly slice and dice models – in the example, slicing and dicing takes 0-1 milliseconds for 1,000 models.

Since Snapshot uses Node.js, the browser support is that of Socket.io, which essentially means Snapshot supports Internet Explorer 5.5+.

Example

  • Browser connects to Snapshot on Node.js server;
  • Snapshot emits snapshot/default/contentUpdated with first page's 50 models;
  • Browser increments the page number;
  • Snapshot emits snapshot/default/contentUpdated with second page's 50 models;
  • Browser applies filter to select only red items;
  • Snapshot emits snapshot/default/contentUpdated to supply second page's red models;
  • Browser sorts the models by their colour;
  • Snapshot emits snapshot/default/contentUpdated to supply second page's red models ordered globally by colour;

Caching

You may wish to cache the collection loaded into Snapshot – for this we recommend something like RedisCache.

snapshot.js's People

Contributors

wildhoney avatar mortonfox avatar neverfox avatar

Stargazers

 avatar Elamurugan Nallathambi avatar vulcangz avatar Limay avatar ymd avatar Daniel Sirakov avatar Chris Hart avatar wen avatar  avatar vlspopov avatar Fabio Dias Rollo avatar Jerzerak avatar Max avatar dengwei avatar  avatar  avatar jian avatar Kumarajiva avatar Aditya avatar Ahnerd avatar mandarin avatar Lloyd Zhou avatar Andrea Sangiorgio avatar Angus H. avatar Will Sahatdjian avatar Hans Scheuren avatar Keyur avatar Talal Suhail avatar Karan Nijhawan avatar Tom Sutton avatar zhangshuibo avatar  avatar Valentin Vichnal avatar Piotrek Majewski avatar Anand Gorantala avatar alvin.z avatar Alexandru Pora avatar  avatar Ye Thu Soe avatar Francesco avatar JT5D avatar Marco Breiter avatar larry avatar Jason Melgoza avatar Mohamed Meabed avatar Casaval avatar Anibal avatar Arkadiusz Szaleniec avatar Francis avatar  avatar Christopher Humphries avatar Vaibhav avatar Alan Plum avatar Peter Tseng avatar Jasmine Hegman avatar  avatar Nikolay avatar  avatar  avatar matt cameron avatar Hüseyin Mert avatar  avatar  avatar Todd Bashor avatar Michael MacDonald avatar jacob avatar Steve Chikwaya avatar  avatar coderaiser avatar JM avatar Daniel Rynaski avatar Alexey Okhrimenko avatar  avatar Thomas Strobl avatar Craig MacKay avatar Vlad Korobov avatar Cyril Hou avatar dayson avatar Alan Lee avatar Glen Ihrig avatar Brandon Brown avatar Operandom avatar Tamim Bin Ibrahim avatar Ax avatar  avatar Oleksandr Bordun avatar Alexey Avramchik avatar Eugene Kostrikov avatar Mikhail Matyunin avatar AB avatar  avatar Mario Vejlupek avatar Manlio Barajas avatar YongHun Byun avatar Jeroen Coumans avatar  avatar Abhijeet Sutar avatar madfrog avatar  avatar Chris Witko avatar

Watchers

yury avatar Jasmine Hegman avatar  avatar timelyportfolio avatar James Cloos avatar hunslater avatar  avatar  avatar Tony Brown avatar  avatar Chime Crisis avatar  avatar  avatar  avatar

snapshot.js's Issues

Trouble with npm install

npm 3.5.2

ENOTEMPTY: directory not empty, rename '/Users/neverfox/Repos/my-repo/node_modules/node-
snapshot' -> '/Users/neverfox/Repos/my-repo/node_modules/.node-snapshot.DELETE'

This is with a clean node_modules. No such directory existed prior to install. I wonder if it has to do with this:

"preinstall": "npm install --ignore-scripts",
"postinstall": "bower install; grunt build:production"

I don't usually see npm scripts calling for more installs, since NPM handles pulling in dependencies, and the library is already built in this dist folder. Furthermore, the client side bower dependencies probably shouldn't be installed as part of postinstall. Thoughts? I could be just misunderstanding what's going on there because I couldn't find any documentation on --ignore-scripts.

Cannot install via npm

Hi,

I am trying your magento-on-angular idea and got this problem while executing npm install
I also tried to install node-snapshot 0.4.6 but still receive the same error.
Would you mind taking a look, please?

Thanks in advance,

-Krist

Here is npm-debug.log

1305 info preinstall [email protected]
1306 verbose unsafe-perm in lifecycle true
1307 info [email protected] Failed to exec preinstall script
1308 info /Users/kristw/workspace/printnista_moa/node_modules/node-snapshot unbuild
1309 info preuninstall [email protected]
1310 info uninstall [email protected]
1311 verbose true,/Users/kristw/workspace/printnista_moa/node_modules,/Users/kristw/workspace/printnista_moa/node_modules unbuild [email protected]
1312 info postuninstall [email protected]
1313 error [email protected] preinstall: `npm install --ignore-scripts`
1313 error Exit status 1
1314 error Failed at the [email protected] preinstall script.
1314 error This is most likely a problem with the node-snapshot package,
1314 error not with npm itself.
1314 error Tell the author that this fails on your system:
1314 error     npm install --ignore-scripts
1314 error You can get their info via:
1314 error     npm owner ls node-snapshot
1314 error There is likely additional logging output above.
1315 error System Darwin 13.3.0
1316 error command "/usr/local/Cellar/node/0.10.22/bin/node" "/usr/local/bin/npm" "install" "--ignore-scripts"
1317 error cwd /Users/kristw/workspace/printnista_moa
1318 error node -v v0.10.22
1319 error npm -v 1.3.14
1320 error code ELIFECYCLE
1321 verbose exit [ 1, true ]

Windowing instead of pagination

I was hoping to be able to implement lazy-loading scrolling instead of pagination using Snapshot.js, but it appears to be limited to that paradigm, possibly for good reasons. But is it currently possible to do that or would that be an enhancement that would have to be developed? I would think that if it could accept something like startingIndex instead of pageNumber, it could work.

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.