Comparing version 1.4.0 to 2.0.0
@@ -1,24 +0,7 @@ | ||
/// <reference path="../@types/automerge/index.d.ts" /> | ||
import { Doc, Message } from "automerge"; | ||
export interface AsyncDocStore { | ||
getDoc<T>(docId: string): Promise<Doc<T>>; | ||
setDoc<T>(docId: string, doc: Doc<T>): Promise<Doc<T>>; | ||
} | ||
export declare class Connection { | ||
private _docStore; | ||
private _sendMsg; | ||
private _ourClockMap; | ||
private _theirClockMaps; | ||
constructor(params: { | ||
store: AsyncDocStore; | ||
sendMsg: (peerId: string, msg: Message) => void; | ||
}); | ||
addPeer(peerId: string): void; | ||
docChanged(docId: string, doc: Doc<any>): Promise<void>; | ||
receiveMsg(peerId: string, msg: Message): Promise<void | import("automerge").FreezeObject<unknown>>; | ||
private sendMsg; | ||
private updateOurClock; | ||
private createMsg; | ||
private syncDoc; | ||
private maybeSyncDocWithPeer; | ||
} | ||
import Hub from "./hub"; | ||
import Peer from "./peer"; | ||
declare const _default: { | ||
Hub: typeof Hub; | ||
Peer: typeof Peer; | ||
}; | ||
export default _default; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const automerge_1 = require("automerge"); | ||
const immutable_1 = require("immutable"); | ||
const invariant = require("invariant"); | ||
const lessOrEqual_1 = require("./lessOrEqual"); | ||
// Updates the vector clock for `docId` in `clockMap` (mapping from docId to vector clock) | ||
// by merging in the new vector clock `clock`. Returns the updated `clockMap`, in which each node's | ||
// sequence number has been set to the maximum for that node. | ||
function clockUnion(clockMap, docId, clock) { | ||
clock = clockMap.get(docId, immutable_1.Map()).mergeWith((x, y) => Math.max(x, y), clock); | ||
return clockMap.set(docId, clock); | ||
} | ||
// Keeps track of the communication with one particular peer. Allows updates | ||
// for many documents to be multiplexed over a single connection. | ||
class Connection { | ||
constructor(params) { | ||
this._docStore = params.store; | ||
this._sendMsg = params.sendMsg; | ||
this._ourClockMap = immutable_1.Map(); | ||
this._theirClockMaps = immutable_1.Map(); | ||
} | ||
// Manually adds a peer that we should talk to | ||
addPeer(peerId) { | ||
if (this._theirClockMaps.has(peerId)) { | ||
return; // do nothing if we already have this peer | ||
} | ||
this._theirClockMaps = this._theirClockMaps.set(peerId, immutable_1.Map({})); | ||
} | ||
// manually call this when you want to change the document on the network. | ||
async docChanged(docId, doc) { | ||
const state = automerge_1.Frontend.getBackendState(doc); | ||
const clock = state.getIn(["opSet", "clock"]); | ||
if (!clock) { | ||
throw new TypeError("This object cannot be used for network sync. " + | ||
"Are you trying to sync a snapshot from the history?"); | ||
} | ||
if (!lessOrEqual_1.default(this._ourClockMap.get(docId, immutable_1.Map()), clock)) { | ||
throw new RangeError("Cannot pass an old state object to a connection"); | ||
} | ||
await this.syncDoc(docId); | ||
} | ||
async receiveMsg(peerId, msg) { | ||
if (!peerId || typeof peerId !== "string") { | ||
throw new Error(`receiveMsg got a peerId that's not a string`); | ||
} | ||
invariant(this._theirClockMaps.keySeq().includes(peerId), `receivedMsg called with unknown peer '${peerId}'. Peers must be registered first with "conn.addPeer('${peerId}')"`); | ||
if (msg.clock) { | ||
this._theirClockMaps = this._theirClockMaps.set(peerId, clockUnion(this._theirClockMaps.get(peerId), msg.docId, immutable_1.fromJS(msg.clock))); | ||
} | ||
if (msg.changes) { | ||
let doc = await this._docStore.getDoc(msg.docId); | ||
if (!doc) { | ||
doc = automerge_1.init({}); | ||
} | ||
const newDoc = automerge_1.applyChanges(doc, msg.changes); | ||
await this._docStore.setDoc(msg.docId, newDoc); | ||
return this.syncDoc(msg.docId); | ||
} | ||
if (await this._docStore.getDoc(msg.docId)) { | ||
this.syncDoc(msg.docId); | ||
} | ||
else if (!this._ourClockMap.has(msg.docId)) { | ||
// If the remote node has data that we don't, immediately ask for it. | ||
// TODO should we sometimes exercise restraint in what we ask for? | ||
this.sendMsg(peerId, this.createMsg(msg.docId, immutable_1.Map())); | ||
} | ||
return this._docStore.getDoc(msg.docId); | ||
} | ||
sendMsg(peerId, msg) { | ||
this.updateOurClock(msg.docId, msg.clock); | ||
this._sendMsg(peerId, msg); | ||
} | ||
updateOurClock(docId, clock) { | ||
this._ourClockMap = clockUnion(this._ourClockMap, docId, clock); | ||
} | ||
createMsg(docId, clock, changes) { | ||
const msg = { | ||
docId, | ||
clock: clock.toJS() | ||
}; | ||
if (changes) | ||
msg.changes = changes; | ||
return msg; | ||
} | ||
// Syncs document with everyone. | ||
async syncDoc(docId) { | ||
for (let peerId of this._theirClockMaps.keys()) { | ||
await this.maybeSyncDocWithPeer(peerId, docId); | ||
} | ||
} | ||
async maybeSyncDocWithPeer(theirPeerId, docId) { | ||
const doc = await this._docStore.getDoc(docId); | ||
if (!doc) { | ||
throw new Error(`Couldn't find doc with id '${docId}'`); | ||
} | ||
const state = automerge_1.Frontend.getBackendState(doc); | ||
const clock = state.getIn(["opSet", "clock"]); | ||
const changes = automerge_1.Backend.getMissingChanges(state, this._theirClockMaps.get(theirPeerId).get(docId, immutable_1.Map())); | ||
// if we have changes we need to sync, do so. | ||
if (changes.length > 0) { | ||
this._theirClockMaps = this._theirClockMaps.set(theirPeerId, clockUnion(this._theirClockMaps.get(theirPeerId), docId, clock)); | ||
this.sendMsg(theirPeerId, this.createMsg(docId, clock, changes)); | ||
return; | ||
} | ||
const ourClockIsOutOfSync = !clock.equals(this._ourClockMap.get(docId, immutable_1.Map())); | ||
if (ourClockIsOutOfSync) { | ||
// Note: updates ourClock AND sends a message. | ||
this.sendMsg(theirPeerId, this.createMsg(docId, clock)); | ||
} | ||
} | ||
} | ||
exports.Connection = Connection; | ||
const hub_1 = require("./hub"); | ||
const peer_1 = require("./peer"); | ||
exports.default = { | ||
Hub: hub_1.default, | ||
Peer: peer_1.default | ||
}; |
{ | ||
"name": "manymerge", | ||
"version": "1.4.0", | ||
"version": "2.0.0", | ||
"main": "dist/index.js", | ||
@@ -35,4 +35,5 @@ "author": "Evan Conrad", | ||
"@types/events": "^3.0.0", | ||
"automerge-clocks": "^1.0.0", | ||
"invariant": "^2.2.4" | ||
} | ||
} |
# ManyMerge | ||
ManyMerge is a client for [Automerge](https://github.com/automerge/automerge) that, unlike the existing [Automerge.Connection](https://github.com/automerge/automerge/blob/master/src/connection.js), sends and receives changes from _multiple peers_ at once. | ||
ManyMerge is a protocol for synchronizing Automerge documents. It's a replacement for `Automerge.Connection` that supports many-to-many and one-to-many relationships. | ||
ManyMerge is network-opinionated, but lets you implement it yourself. It assumes you have the concept of "broadcasting", where everyone who has access to the document can be alerted of a message. It also assumes that you're keeping a unique id for each connection on your network (called the `peerId`). | ||
## Install | ||
``` | ||
npm install --save manymerge | ||
``` | ||
## Usage | ||
Manymerge comes with two different types of connections that work together: **Peers** and **Hubs**. | ||
### Peers | ||
A Peer is **a 1-1 relationship** that can talk to a Hub or another Peer. Your peer will need to create a `sendMsg` function that takes a ManyMerge `Message` and sends it to the network. Typically that looks like this: | ||
```ts | ||
import { Peer } from "manymerge"; | ||
function sendMsg(msg) { | ||
MyNetwork.emit("to-server", msg); | ||
} | ||
const peer = new Peer(sendMsg); | ||
``` | ||
When a peer wants to alert it's counterpart that it changed a document, it should call the `notify` function: | ||
```ts | ||
import Automerge from "automerge"; | ||
let myDoc = Automerge.from({ title: "cool doc" }); | ||
peer.notify(myDoc); | ||
``` | ||
When a peer gets a message from the network, it should run `applyMessage`, which will return a new document | ||
with any changes applied. | ||
```ts | ||
let myDoc = Automerge.from({ title: "cool doc" }); | ||
MyNetwork.on("from-server", msg => { | ||
myDoc = peer.applyMessage(msg, myDoc); | ||
}); | ||
``` | ||
### Hubs | ||
Hubs are a **many-to-many (or 1-to-many) relationship** that can talk to many Peers or other Hubs. Unlike Peers, Hubs need the ability | ||
to "broadcast" a message to everyone on the network (or at least as many people as possible).To save time, Hubs will also cache Peer's they've seen recently and directly communicate directly with them. | ||
To set this up, create `broadcastMsg` and `sendMsgTo` functions: | ||
```ts | ||
import { Hub } from "manymerge"; | ||
function sendMsgTo(peerId, msg) { | ||
MyNetwork.to(peerId).emit("msg", msg); | ||
} | ||
function broadcastMsg(msg) { | ||
MyNetwork.on("some-channel").emit("msg", msg); | ||
} | ||
const hub = new Hub(sendMsgTo, broadcastMsg); | ||
``` | ||
Then, hub works like a peer, it can notify others of documents: | ||
```ts | ||
// Tell folks about our doc | ||
hub.notify(myDoc); | ||
``` | ||
Unlike the peer, when it gets a message, it'll need to know the unique id of the connection sending it. It will use this later in the `sendMsgTo` function. | ||
```ts | ||
MyNetwork.on("msg", (from, msg) => { | ||
myDoc = hub.applyMessage(from, msg, myDoc); | ||
}); | ||
``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
33146
26
786
86
5
+ Addedautomerge-clocks@^1.0.0
+ Addedautomerge@0.13.0(transitive)
+ Addedautomerge-clocks@1.2.1(transitive)