Mesh is a universal interface for communicating with data sources whether it's your API, mongodb, pubnub, webrtc, socket.io, redis, or local storage. Easily build sophisticated features such as offline-mode, realtime data, rollbacks, and more with little effort.
Mesh is entirely customizable, and doesn't make assumptions about how a data source works. You can easily build your own API adapter that's interoperable with all the other mesh plugins.
Here's a basic example of how you might implement an API that caches temporarily to local storage:
var mesh = require("mesh");
var http = require("mesh-http");
var localStorage = require("mesh-local-storage");
// local storage cache - keep stuff for one minute max
var cache = localStorage({
ttl: 1000 * 60
});
var api = http({
prefix: "/api"
});
// pipe all persistence operations to the cache
api(mesh.op("tail")).pipe(mesh.open(cache));
// the DB we'll use, return the first result returned, and
// only pass 'load' operations to the cache
var db = mesh.first(mesh.accept("load", cache), api);
db(mesh.op("insert", {
collection: "people"
// path is automatically resolved from the collection param,
// but you can easily override it.
path: "/people",
// POST is resolved from the operation name, but it's
// also overridable
method: "POST",
data: {
name: "john"
}
})).on("data", function(personData) {
// load the person saved. This should result in a cache
// hit for local storage. Also note that the HTTP path & method
// will automatically get resolved.
db(mesh.op("load", {
collection: "people",
query: {
name: person.name
}
})).
on("data", function(personData) {
// do stuff with data
});
});
- Streamable interface.
- Works with any library, or framework.
- Works on any platform.
- Tiny (11kb).
- Works nicely with other stream-based libraries such as highland.
- Isomorphic. Easily use different databases for different platforms.
- Easily testable. Stub out any database for a fake one.
- Simple design. Use it for many other things such as an event bus, message-queue service, etc.
- Examples
- MeteorJS clone with ReactJS
- latency compensation
- same views & models on the front-end & backend
- offline-mode (fully operational with CRUD)
- realtime data
- mongodb queries server-side/client-side with socket.io
- MeteorJS clone with ReactJS
- Adapters
- RPC adapter
- ReactJS
- AngularJS Adapter
- EmberJS
npm install mesh
Or via bower:
bower install mesh
- pubnub - pubnub sync adapter
- socket.io - socket.io sync adapter
- webrtc - webrtc sync adapter
- loki - loki in-memory database
- memory - another in-memory database
- local-storage - local storage database
- http - HTTP adapter
- mongodb - Mongodb Adapter (server-side)
Below is an example of a realtime DB that uses pubnub, and local storage.
var mesh = require("mesh");
var pubnub = require("mesh-pubnub");
var localStorage = require("mesh-local-storage");
// store data locally on the users machine
var localdb = localStorage();
// pubnub adapter for sending operations to other connected clients
var pubdb = pubnub({
publishKey: "publish key",
subscribeKey: "subscribe key",
channel: "chatroom"
});
// the actual DB we're going to use. Pass
// all operations to localstorage, and pubnub
var db = mesh.parallel(localdb, pubdb);
// tail all operations from pubnub to the local DB. Note
// that remote operations don't get re-sent to pubnub.
pubdb(mesh.op("tail")).pipe(mesh.open(db));
// create a child database - collection will get passed to each operation
var peopleDb = mesh.child(db, {
collection: "people"
});
// insert some people
peopleDb(mesh.op("insert", {
data: [
{ name: "Gordon Ramsay" },
{ name: "Ben Stiller" }
]
})).on("data", function() {
// handle data here
});
stream.Readable db(operationName, options)
Runs a new operation.
Note that the supported operations & required options may change depending on the data store you're using.
var localStorage = require("mesh-local-storage");
var localdb = localStorage();
localdb(mesh.operation("insert", {
collection: "people",
data: { name: "Arnold Schwarzenegger" }
})).on("data", function() {
// handle data here
});
stream.Stream mesh.open(db)
Creates a new operation stream.
var operationStream = mesh.open(db);
// emitted when the operation is performed
operationStream.on("data", function() {
});
operationStream.write(mesh.operation("insert", {
collection: "people",
data: { name: "Sandra Bullock" }
}));
operationStream.write(mesh.operation("remove", {
collection: "people",
query: { name: "Jeff Goldbloom" }
}));
creates a new operation which can be written to a database stream. See mesh.open(db)
.
mesh.open(db).write(mesh.operation("insert", {
collection: "friends",
data: { name: "Blakers" }
}));
shorthand for mesh.operation(name options)
.
to operation
- Makes it so that you can simply call db(operationName, options)
instead of passing in the operation
each time.
var db = mesh.top(localStorage());
// enables this
db("insert", {
collection: "people",
data: { name: "Jorge" }
});
// also accepts this
db(mesh.operation("insert"));
Creates a new child database. options
is essentially just added to each operation performed.
var peopleDb = mesh.top(mesh.child(db, { collection: "people" }));
// insert a new person into the people collection
peopleDb("insert", {
data: { name: "Shrek" }
});
Makes the db tailable. This simply allows you to listen for any operations invoked on a db such as create
, update
, remove
, and load
.
reject
is an array of operations to ignore. Default is [load]
.
var db = mesh.tailable(localdb);
db("tail", function() {
});
var peopleDb = mesh.top(mesh.child(db, { collection: "people" }));
peopleDb("insert", { data: { name: "Donkey" }}); // trigger tail
peopleDb("remove", { query: { name: "Donkey" }}); // trigger tail
peopleDb("update", { query: { name: "Donkey" }, data: { name: "Donkay" }}); // trigger tail
peopleDb("load", { query: { name: "Donkey" }}); // ignored by tail
Combines databases and executes operations in parallel.
var db = mesh.parallel(localdb, httpdb);
// execute "load" on localdb at the same time
db(mesh.op("load")).on("data", function() {
// Note that his will get called TWICE
}).on("end", function() {
// called when operation is executed on all dbs
});
Combines databases and executes operations in sequence.
var db = mesh.top(mesh.sequence(localdb, httpdb));
// load data from localdb first, then move to httpdb
db("load").on("data", function() {
// Note that his will get called TWICE
});
Runs dbs in sequence, but stops when a result is emitted from a database.
var db = mesh.top(mesh.first(localStorage(), http()));
// load data from local storage if it exists, or continue
// to http storage
db("load", { collection: "people" }).on("data", function() {
});
Accepts only the provided operations.
// main DB - api server
var httpdb = mesh.tailable(http());
// temporary cache
var localdb = localStorage();
// main DB - get cached data from local storage before
// checking the server
var db = mesh.first(mesh.accept("load", localdb), httpdb);
// pipe all persistence operations back to local storage
httpdb(mesh.op("tail")).pipe(mesh.open(localdb));
Runs all operations except the ones provided.
Runs a database operation
mesh.run(peopleDb, "insert", { data: { name: "blarg"}}, function(err, insertedItem) {
// handle result here
});
wraps a callback as a db handler
var dispatch = mesh.parallel(
mesh.accept("load", mesh.wrapCallback(function(operation, next) {
// do stuff
})),
mesh.accept("showPopup", mesh.wrapCallback(function(operation, next) {
document.body.appendChild(operation.element);
next();
}))
);
dispatch(crud.op("showPopup", { element: document.createTextNode("Hello!") }));
Building a custom database is pretty easy. All you need to do
is return a stream when db(opName, options)
is called.
Here's some scaffolding for a custom db:
// slimmed down version of node streams.
var stream = require("obj-stream");
function createDatabase(options) {
// create database here
// return fn that executes operations
return function(operation) {
var writable = stream.writable();
// this is important so that data can be piped to other things
process.nextTick(function() {
// collection MUST exist
if (!operation.collection) return writable.reader.emit("error", new Error("missing collection"));
// perform task here
// write data from insert/load
writable.write(data);
// must call end operation when complete
writable.end();
});
return writable.reader;
};
}
Keep in mind that there are a few conventions you should follow when writing custom database adapters. These conventions are here to ensure that databases are interoperable with each other.
Insert a new item in the database. Note that data
is emitted for each item inserted in the database.
options
- db optionsdata
- data to insert. Accepts 1 or many itemscollection
- collection to insert (optional for dbs that don't have it)
var _ = require("highland");
var peopleDb = mesh.top(mesh.child(db, { collection: "people" }));
// insert one item
peopleDb("insert", {
data: { name: "jeff" }
});
// insert many items & collect the results in 1
// array
peopleDb("insert", {
data: [
{ name: "Joe" },
{ name: "Rogan" }
]
}).pipe(_.pipeline(_.collect)).on("data", function(people) {
});
Updates an item in the database. Doesn't return any values.
options
query
- search query for items to updatedata
- data to merge withcollection
- db collectionmulti
-true
to update multiple items.false
is default.
var peopleDb = mesh.top(mesh.child(db, {
collection: "people"
}));
peopleDb("update", {
query: { name: "jake" },
data: { age: 17 }
});
// update multiple items
peopleDb("update", {
multi: true,
query: { name: "joe" },
data: { age: 17 }
});
Updates an item if it exists. Inserts an item if it doesn't.
options
query
- search query for items to updatedata
- data to merge or insertcollection
- db collection
var peopleDb = mesh.top(mesh.child(db, {
collection: "people"
}));
// insert
peopleDb("upsert", {
query: { name: "jake" },
data: {
name: "jake",
age: 17
}
}).on("end", function() {
// update
peopleDb("upsert", {
query: { name: "jake" },
data: {
name: "jake",
age: 18
}
})
});
Loads one or many items from the database.
options
query
- search query for itemscollection
- db collectionmulti
-true
if loading multiple.false
is default.
var peopleDb = mesh.top(mesh.child(db, { collection: "people" }));
// load one item
peopleDb("load", {
query: { name: "tina" }
}).on("data", function() {
// handle
});
// load many items
peopleDb("load", {
multi: true,
query: { name: "tina" }
}).pipe(_.pipeline(_.collect)).on("data", function(people) {
// handle
});
Removes an item from the database.
options
query
- query to searchcollection
- collection to searchmulti
-true
to remove multiple items
// remove one item
db("remove", {
collection: "people",
query: { name: "batman" }
});
// remove all instances where age = 54
db("remove", {
collection: "people",
query: { age: 54 },
multi: true
});