This library is an alpha release and is in active development. Please expect breaking changes!
Build local-first, distributed, decentralized applications
What does local-first, distributed, decentralized applications
mean?
Up until recently, you had two options when creating an application:
Each of these types of applications has its strengths (and weaknesses). For example, local apps are typically fast because they don't need to talk to servers over slow networks. However, if your app needs multiple users to collaborate on common tasks, then a local application won't work for you; you'll probably need to create a client/server application, right? Well, maybe, but now a third option allows you to combine many of the best features from local and client/server applications. It's a new, best-of-both-worlds paradigm.
Imagine you wanted to build a basic contacts application that you can share with the people in your household.
Ideally, anyone in your house should be able to create, update and delete shared contacts. You might be inclined to make this a typical client/server application, but you're not restricted to client/server anymore because of a recent innovation called Conflict-free Replicated Data Types (CRDTs).
CRDTs are a collection of data types similar to other data types you're probably familiar with, such as Arrays, Maps, and Sets.
The interesting thing about CRDTs is that any local changes you make to a CRDT can be copied and merged into a replica on a different computer without any conflicts. So, for example, in your contacts application, you could add, update and delete some of our shared contacts, and the changes you make are guaranteed to replicate and merge with my contacts without conflict. Even if we both make edits to the same contact, at the same time, there are mechanisms to synchronize our changes in a conflict-free way.
CRDTs are transport agnostic, meaning you can use any method to send updates from one replica to another. For example, you could use WebSockets, Bluetooth, or even sneakernet. However, peer-to-peer options such as WebRTC are usually preferred.
A local first application means everything your application needs to function is co-located with the application; in other words, all the logic and data required to run your application is on your computing device. This means your application runs fast, and it works when offline, so for example, if your car gets a flat in a rural area with no data coverage, you can still access your contacts and call for help.
Your contacts application is distributed because it doesn't just live on one device; it lives on every device in your household.
Because there is no single source of truth, ie no central server, your contacts application is decentralized.
You don't need to know exactly how CRDTs work to use them; you just need to know how to use our APIs. But if you're interested in learning more about CRDTs, you can check out crdt.tech or watch our introductory video.
npm install @mycelial/web # or @mycelial/nodejs
npm install @mycelial/v0
npm install @mycelial/websocket
import * as Mycelial from '@mycelial/web'; // or '@mycelial/nodejs'
import { Store, Entity } from '@mycelial/v0';
import * as Websocket from '@mycelial/websocket';
// NOTE: if you use a shared public relay, you'll want to use a unique namespace.
// The namespace is used as a pub/sub topic, so ensuring it's unique will avoid
// namespace collisions.
const instance = Mycelial.create("mycelial/contacts")
const disconnect = Websocket.create(instance, {
endpoint: 'wss://v0alpha-relay.fly.dev/v0alpha'
});
const store = new Store(instance);
Subscribe to changes.
#subscribe
method to listen for changes in the store:
const unsubscribe = store.subscribe((store) => {
console.log('something changed')
});
Create an entity by passing it a unique id followed by an object.
const contact = Entity.from('<unique-contact-id>', {
kind: 'contact',
name: 'James M.',
email: 'james@mycelial.com',
phone: '5555555555',
archived: false
});
store.add(contact);
const contacts = store.filter(
(entity) =>
entity.properties.kind === 'contact' &&
entity.properties.archived === false,
);
for (const contact of contacts) {
console.log(contact.properties);
}
Complete Snippet
import * as Mycelial from '@mycelial/web'; // or '@mycelial/nodejs'
import { Store, Entity } from '@mycelial/v0';
import * as Websocket from '@mycelial/websocket';
const instance = Mycelial.create("contacts")
const disconnect = Websocket.create(instance, {
endpoint: 'wss://v0alpha-relay.fly.dev/v0alpha'
});
const store = new Store(instance);
const unsubscribe = store.subscribe((store) => {
console.log('something changed')
});
const contact = Entity.from('<unique-contact-id>', {
kind: 'contact',
name: 'James M.',
email: 'james@mycelial.com',
phone: '5555555555',
archived: false
});
store.add(contact);
const contacts = store.filter(
(entity) =>
entity.properties.kind === 'contact' &&
entity.properties.archived === false,
);
for (const contact of contacts) {
console.log(contact.properties);
}
import * as Mycelial from '@mycelial/web'; // or '@mycelial/nodejs'
Create an instance of our core library. The namespace is used as a topic for pub/sub.
import * as Websocket from '@mycelial/websocket';
const instance = Mycelial.create("contacts");
const disconnect = Websocket.create(instance, {
endpoint: 'wss://v0alpha-relay.fly.dev/v0alpha'
});
disconnect();
Creates a WebSocket adapter that synchronizes the replica across two or more nodes. The return value is a function that disconnects the websocket when called.
import { Entity, Store } from '@mycelial/v0';
Instantiate a new store, passing in the CRDT instance.
Subscribe to all store changes, both remote and local. The return value is a function that can be called to unsubscribe.
Add the provided entity to its index, returns a clean instance back.
Create entity from id and object
id
– a unique key of the entityobject
– actual dataUpdate the entity, returns a new instance
object
– the data to update the entity withApache 2.0