Comparing version 0.6.0 to 0.7.0
{ | ||
"name": "automerge", | ||
"version": "0.6.0", | ||
"version": "0.7.0", | ||
"description": "Data structures for building collaborative applications", | ||
@@ -5,0 +5,0 @@ "main": "src/automerge.js", |
@@ -52,3 +52,3 @@ # Automerge | ||
peer-to-peer model using [WebRTC](https://webrtc.org/). | ||
* **Immutable state**. A Automerge object is an immutable snapshot of the application state at one | ||
* **Immutable state**. An Automerge object is an immutable snapshot of the application state at one | ||
point in time. Whenever you make a change, or merge in a change that came from the network, you | ||
@@ -158,3 +158,3 @@ get back a new state object reflecting that change. This fact makes Automerge compatible with the | ||
// [ { title: 'Rewrite everything in Haskell', done: true }, | ||
// { title: 'Rewrite everything in Clojure', done: false } } | ||
// { title: 'Rewrite everything in Clojure', done: false } ] } | ||
@@ -489,3 +489,3 @@ // And, unbeknownst to device 1, also make a change on device 2: | ||
* No integrity checking: if a buggy (or malicious) device makes corrupted edits, it can cause | ||
the application state on other devices to be come corrupted or go out of sync. | ||
the application state on other devices to become corrupted or go out of sync. | ||
* No security: there is currently no encryption, authentication, or access control. | ||
@@ -492,0 +492,0 @@ * Small number of collaborators: Automerge is designed for small-group collaborations. While there |
@@ -5,10 +5,8 @@ const { Map, List, fromJS } = require('immutable') | ||
const OpSet = require('./op_set') | ||
const {isObject, checkTarget, makeChange, merge, applyChanges} = require('./auto_api') | ||
const FreezeAPI = require('./freeze_api') | ||
const ImmutableAPI = require('./immutable_api') | ||
const { Text } = require('./text') | ||
const transit = require('transit-immutable-js') | ||
function isObject(obj) { | ||
return typeof obj === 'object' && obj !== null | ||
} | ||
function makeOp(state, opProps) { | ||
@@ -116,11 +114,2 @@ const opSet = state.get('opSet'), actor = state.get('actorId'), op = fromJS(opProps) | ||
function makeChange(root, newState, message) { | ||
const actor = root._state.get('actorId') | ||
const seq = root._state.getIn(['opSet', 'clock', actor], 0) + 1 | ||
const deps = root._state.getIn(['opSet', 'deps']).remove(actor) | ||
const change = fromJS({actor, seq, deps, message}) | ||
.set('ops', newState.getIn(['opSet', 'local'])) | ||
return FreezeAPI.applyChanges(root, List.of(change), true) | ||
} | ||
///// Automerge.* API | ||
@@ -132,13 +121,4 @@ | ||
function checkTarget(funcName, target, needMutable) { | ||
if (!target || !target._state || !target._objectId || | ||
!target._state.hasIn(['opSet', 'byObject', target._objectId])) { | ||
throw new TypeError('The first argument to Automerge.' + funcName + | ||
' must be the object to modify, but you passed ' + JSON.stringify(target)) | ||
} | ||
if (needMutable && (!target._change || !target._change.mutable)) { | ||
throw new TypeError('Automerge.' + funcName + ' requires a writable object as first argument, ' + | ||
'but the one you passed is read-only. Please use Automerge.change() ' + | ||
'to get a writable version.') | ||
} | ||
function initImmutable(actorId) { | ||
return ImmutableAPI.init(actorId || uuid()) | ||
} | ||
@@ -191,2 +171,6 @@ | ||
function loadImmutable(string, actorId) { | ||
return ImmutableAPI.applyChanges(ImmutableAPI.init(actorId), transit.fromJSON(string), false) | ||
} | ||
function save(doc) { | ||
@@ -229,13 +213,2 @@ checkTarget('save', doc) | ||
function merge(local, remote) { | ||
checkTarget('merge', local) | ||
if (local._state.get('actorId') === remote._state.get('actorId')) { | ||
throw new RangeError('Cannot merge an actor with itself') | ||
} | ||
const clock = local._state.getIn(['opSet', 'clock']) | ||
const changes = OpSet.getMissingChanges(remote._state.get('opSet'), clock) | ||
return FreezeAPI.applyChanges(local, changes, true) | ||
} | ||
// Returns true if all components of clock1 are less than or equal to those of clock2. | ||
@@ -270,2 +243,19 @@ // Returns false if there is at least one component in which clock1 is greater than clock2 | ||
function getConflicts(doc, list) { | ||
checkTarget('getConflicts', doc) | ||
const opSet = doc._state.get('opSet') | ||
const objectId = list._objectId | ||
if (!objectId || opSet.getIn(['byObject', objectId, '_init', 'action']) !== 'makeList') { | ||
throw new TypeError('The second argument to Automerge.getConflicts must be a list object') | ||
} | ||
const context = { | ||
cache: {}, | ||
instantiateObject (opSet, objectId) { | ||
return opSet.getIn(['cache', objectId]) | ||
} | ||
} | ||
return List(OpSet.listIterator(opSet, objectId, 'conflicts', context)) | ||
} | ||
function getChanges(oldState, newState) { | ||
@@ -283,12 +273,22 @@ checkTarget('getChanges', oldState) | ||
function applyChanges(doc, changes) { | ||
checkTarget('applyChanges', doc) | ||
return FreezeAPI.applyChanges(doc, fromJS(changes), true) | ||
function getChangesForActor(state, actorId) { | ||
checkTarget('getChanges', state) | ||
// I might want to validate the actorId here | ||
return OpSet.getChangesForActor(state._state.get('opSet'), actorId).toJS() | ||
} | ||
function getMissingDeps(doc) { | ||
checkTarget('getMissingDeps', doc) | ||
return OpSet.getMissingDeps(doc._state.get('opSet')) | ||
} | ||
module.exports = { | ||
init, change, merge, diff, assign, load, save, equals, inspect, getHistory, | ||
getChanges, applyChanges, Text, | ||
initImmutable, loadImmutable, getConflicts, | ||
getChanges, getChangesForActor, applyChanges, getMissingDeps, Text, | ||
DocSet: require('./doc_set'), | ||
WatchableDoc: require('./watchable_doc'), | ||
Connection: require('./connection') | ||
} |
@@ -72,17 +72,35 @@ const { Map, List, Set } = require('immutable') | ||
let list = [] | ||
Object.defineProperty(list, '_objectId', {value: edit.obj}) | ||
Object.defineProperty(list, '_objectId', {value: edit.obj}) | ||
Object.defineProperty(list, '_conflicts', {value: Object.freeze([])}) | ||
return opSet.setIn(['cache', edit.obj], Object.freeze(list)) | ||
} | ||
let value = edit.link ? opSet.getIn(['cache', edit.value]) : edit.value | ||
let list = opSet.getIn(['cache', edit.obj]).slice() | ||
Object.defineProperty(list, '_objectId', {value: edit.obj}) | ||
let conflict = null | ||
if (edit.conflicts) { | ||
conflict = {} | ||
for (let c of edit.conflicts) { | ||
conflict[c.actor] = c.link ? opSet.getIn(['cache', c.value]) : c.value | ||
} | ||
Object.freeze(conflict) | ||
} | ||
let list = opSet.getIn(['cache', edit.obj]) | ||
const value = edit.link ? opSet.getIn(['cache', edit.value]) : edit.value | ||
const conflicts = list._conflicts.slice() | ||
list = list.slice() // shallow clone | ||
Object.defineProperty(list, '_objectId', {value: edit.obj}) | ||
Object.defineProperty(list, '_conflicts', {value: conflicts}) | ||
if (edit.action === 'insert') { | ||
list.splice(edit.index, 0, value) | ||
conflicts.splice(edit.index, 0, conflict) | ||
} else if (edit.action === 'set') { | ||
list[edit.index] = value | ||
conflicts[edit.index] = conflict | ||
} else if (edit.action === 'remove') { | ||
list.splice(edit.index, 1) | ||
conflicts.splice(edit.index, 1) | ||
} else throw 'Unknown action type: ' + edit.action | ||
Object.freeze(conflicts) | ||
return opSet.setIn(['cache', edit.obj], Object.freeze(list)) | ||
@@ -95,10 +113,31 @@ } | ||
let changed = false | ||
let list = opSet.getIn(['cache', ref.get('obj')]) | ||
if (!isObject(list[index]) || list[index]._objectId !== ref.get('value')) return opSet | ||
const value = opSet.getIn(['cache', ref.get('value')]) | ||
const conflicts = list._conflicts.slice() | ||
list = list.slice() // shallow clone | ||
Object.defineProperty(list, '_objectId', {value: ref.get('obj')}) | ||
list[index] = opSet.getIn(['cache', ref.get('value')]) | ||
Object.defineProperty(list, '_objectId', {value: ref.get('obj')}) | ||
Object.defineProperty(list, '_conflicts', {value: conflicts}) | ||
return opSet.setIn(['cache', ref.get('obj')], Object.freeze(list)) | ||
if (isObject(list[index]) && list[index]._objectId === ref.get('value')) { | ||
list[index] = value | ||
changed = true | ||
} | ||
if (isObject(conflicts[index])) { | ||
for (let actor of Object.keys(conflicts[index])) { | ||
const conflict = conflicts[index][actor] | ||
if (isObject(conflict) && conflict._objectId === ref.get('value')) { | ||
conflicts[index] = Object.assign({}, conflicts[index]) | ||
conflicts[index][actor] = value | ||
Object.freeze(conflicts[index]) | ||
changed = true | ||
} | ||
} | ||
} | ||
if (changed) { | ||
Object.freeze(conflicts) | ||
opSet = opSet.setIn(['cache', ref.get('obj')], Object.freeze(list)) | ||
} | ||
return opSet | ||
} | ||
@@ -162,3 +201,5 @@ | ||
obj = [...OpSet.listIterator(opSet, objectId, 'values', this)] | ||
Object.defineProperty(obj, '_objectId', {value: objectId}) | ||
const conflicts = List(OpSet.listIterator(opSet, objectId, 'conflicts', this)).toJS() | ||
Object.defineProperty(obj, '_objectId', {value: objectId}) | ||
Object.defineProperty(obj, '_conflicts', {value: Object.freeze(conflicts)}) | ||
} else if (objType === 'makeText') { | ||
@@ -165,0 +206,0 @@ obj = new Text(opSet, objectId) |
@@ -72,18 +72,29 @@ const { Map, List, Set } = require('immutable') | ||
function patchList(opSet, objectId, index, action, op) { | ||
function getConflicts(ops) { | ||
const conflicts = [] | ||
for (let op of ops.shift()) { | ||
let conflict = {actor: op.get('actor'), value: op.get('value')} | ||
if (op.get('action') === 'link') conflict.link = true | ||
conflicts.push(conflict) | ||
} | ||
return conflicts | ||
} | ||
function patchList(opSet, objectId, index, action, ops) { | ||
const objType = opSet.getIn(['byObject', objectId, '_init', 'action']) | ||
const firstOp = ops ? ops.first() : null | ||
let elemIds = opSet.getIn(['byObject', objectId, '_elemIds']) | ||
let value = op ? op.get('value') : null | ||
let value = firstOp ? firstOp.get('value') : null | ||
let edit = {action, type: (objType === 'makeText') ? 'text' : 'list', obj: objectId, index} | ||
if (op && op.get('action') === 'link') { | ||
if (firstOp && firstOp.get('action') === 'link') { | ||
edit.link = true | ||
value = {obj: op.get('value')} | ||
value = {obj: firstOp.get('value')} | ||
} | ||
if (action === 'insert') { | ||
elemIds = elemIds.insertIndex(index, op.get('key'), value) | ||
edit.value = op.get('value') | ||
elemIds = elemIds.insertIndex(index, firstOp.get('key'), value) | ||
edit.value = firstOp.get('value') | ||
} else if (action === 'set') { | ||
elemIds = elemIds.setValue(op.get('key'), value) | ||
edit.value = op.get('value') | ||
elemIds = elemIds.setValue(firstOp.get('key'), value) | ||
edit.value = firstOp.get('value') | ||
} else if (action === 'remove') { | ||
@@ -93,2 +104,3 @@ elemIds = elemIds.removeIndex(index) | ||
if (ops && ops.size > 1) edit.conflicts = getConflicts(ops) | ||
opSet = opSet.setIn(['byObject', objectId, '_elemIds'], elemIds) | ||
@@ -107,3 +119,3 @@ return [opSet, [edit]] | ||
} else { | ||
return patchList(opSet, objectId, index, 'set', ops.first()) | ||
return patchList(opSet, objectId, index, 'set', ops) | ||
} | ||
@@ -124,3 +136,3 @@ | ||
return patchList(opSet, objectId, index + 1, 'insert', ops.first()) | ||
return patchList(opSet, objectId, index + 1, 'insert', ops) | ||
} | ||
@@ -142,10 +154,3 @@ } | ||
if (ops.size > 1) { | ||
edit.conflicts = [] | ||
for (let op of ops.shift()) { | ||
let conflict = {actor: op.get('actor'), value: op.get('value')} | ||
if (op.get('action') === 'link') conflict.link = true | ||
edit.conflicts.push(conflict) | ||
} | ||
} | ||
if (ops.size > 1) edit.conflicts = getConflicts(ops) | ||
} | ||
@@ -286,2 +291,26 @@ return [opSet, [edit]] | ||
function getChangesForActor(opSet, forActor, afterSeq) { | ||
afterSeq = afterSeq || 0 | ||
return opSet.get('states') | ||
.filter((states, actor) => actor === forActor) | ||
.map((states, actor) => states.skip(afterSeq)) | ||
.valueSeq() | ||
.flatten(1) | ||
.map(state => state.get('change')) | ||
} | ||
function getMissingDeps(opSet) { | ||
let missing = {} | ||
for (let change of opSet.get('queue')) { | ||
const deps = change.get('deps').set(change.get('actor'), change.get('seq') - 1) | ||
deps.forEach((depSeq, depActor) => { | ||
if (opSet.getIn(['clock', depActor], 0) < depSeq) { | ||
missing[depActor] = Math.max(depSeq, missing[depActor] || 0) | ||
} | ||
}) | ||
} | ||
return missing | ||
} | ||
function getFieldOps(opSet, objectId, key) { | ||
@@ -421,2 +450,9 @@ return opSet.getIn(['byObject', objectId, key], List()) | ||
case 'elems': return {done: false, value: [index, elem]} | ||
case 'conflicts': | ||
let conflict = null | ||
if (ops.size > 1) { | ||
conflict = ops.shift().toMap() | ||
.mapEntries(([_, op]) => [op.get('actor'), getOpValue(opSet, op, context)]) | ||
} | ||
return {done: false, value: conflict} | ||
} | ||
@@ -433,5 +469,5 @@ } | ||
module.exports = { | ||
init, addLocalOp, addChange, getMissingChanges, | ||
init, addLocalOp, addChange, getMissingChanges, getChangesForActor, getMissingDeps, | ||
getObjectFields, getObjectField, getObjectConflicts, | ||
listElemByIndex, listLength, listIterator, ROOT_ID | ||
} |
@@ -62,3 +62,8 @@ const { List, fromJS } = require('immutable') | ||
} | ||
const deleted = [] | ||
for (let n = 0; n < deleteCount; n++) { | ||
deleted.push(OpSet.listElemByIndex(context.state.get('opSet'), listId, start + n, context)) | ||
} | ||
context.state = context.splice(context.state, listId, start, deleteCount, values) | ||
return deleted | ||
}, | ||
@@ -65,0 +70,0 @@ |
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
205476
28
3673