code refactoring, and documentation
This commit is contained in:
parent
638c575dfc
commit
8745fd64ca
@ -1,12 +1,8 @@
|
||||
language: node_js
|
||||
before_install:
|
||||
- "npm install -g bower"
|
||||
- "bower install"
|
||||
node_js:
|
||||
- "0.12"
|
||||
- "0.11"
|
||||
- "0.10"
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- 0.6
|
||||
|
@ -28,8 +28,8 @@
|
||||
Specify which specs to use!
|
||||
|
||||
Commands:
|
||||
- build
|
||||
Build this library
|
||||
- build:deploy
|
||||
Build this library for deployment (es6->es5, minified)
|
||||
- dev:browser
|
||||
Watch the ./src directory.
|
||||
Builds the library on changes.
|
||||
|
@ -130,7 +130,7 @@ class AbstractConnector {
|
||||
}
|
||||
send (uid, message) {
|
||||
if (this.debug) {
|
||||
console.log(`send ${this.userId} -> ${uid}: ${message.type}`, m);// eslint-disable-line
|
||||
console.log(`send ${this.userId} -> ${uid}: ${message.type}`, m) // eslint-disable-line
|
||||
}
|
||||
}
|
||||
/*
|
||||
@ -141,7 +141,7 @@ class AbstractConnector {
|
||||
return
|
||||
}
|
||||
if (this.debug) {
|
||||
console.log(`receive ${sender} -> ${this.userId}: ${m.type}`, m);// eslint-disable-line
|
||||
console.log(`receive ${sender} -> ${this.userId}: ${m.type}`, m) // eslint-disable-line
|
||||
}
|
||||
if (m.type === 'sync step 1') {
|
||||
// TODO: make transaction, stream the ops
|
||||
@ -212,17 +212,19 @@ class AbstractConnector {
|
||||
this.y.db.apply(m.ops)
|
||||
}
|
||||
}
|
||||
// Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
||||
// that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
|
||||
// too much overhead. Y is very likely to get changed a lot in the future
|
||||
//
|
||||
// Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
|
||||
// we encode the JSON as XML.
|
||||
//
|
||||
// When the HB support encoding as XML, the format should look pretty much like this.
|
||||
//
|
||||
// does not support primitive values as array elements
|
||||
// expects an ltx (less than xml) object
|
||||
/*
|
||||
Currently, the HB encodes operations as JSON. For the moment I want to keep it
|
||||
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
|
||||
too much overhead. Y is very likely to get changed a lot in the future
|
||||
|
||||
Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
|
||||
we encode the JSON as XML.
|
||||
|
||||
When the HB support encoding as XML, the format should look pretty much like this.
|
||||
|
||||
does not support primitive values as array elements
|
||||
expects an ltx (less than xml) object
|
||||
*/
|
||||
parseMessageFromXml (m) {
|
||||
function parseArray (node) {
|
||||
for (var n of node.children) {
|
||||
@ -256,14 +258,16 @@ class AbstractConnector {
|
||||
}
|
||||
parseObject(m)
|
||||
}
|
||||
// encode message in xml
|
||||
// we use string because Strophe only accepts an "xml-string"..
|
||||
// So {a:4,b:{c:5}} will look like
|
||||
// <y a="4">
|
||||
// <b c="5"></b>
|
||||
// </y>
|
||||
// m - ltx element
|
||||
// json - Object
|
||||
/*
|
||||
encode message in xml
|
||||
we use string because Strophe only accepts an "xml-string"..
|
||||
So {a:4,b:{c:5}} will look like
|
||||
<y a="4">
|
||||
<b c="5"></b>
|
||||
</y>
|
||||
m - ltx element
|
||||
json - Object
|
||||
*/
|
||||
encodeMessageToXml (msg, obj) {
|
||||
// attributes is optional
|
||||
function encodeObject (m, json) {
|
||||
|
12
src/Notes.md
Normal file
12
src/Notes.md
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
# Notes
|
||||
|
||||
### Terminology
|
||||
|
||||
* DB: DataBase that holds all the information of the shared object. It is devided into the OS, DS, and SS. This can be a persistent database or an in-memory database. Depending on the type of database, it could make sense to store OS, DS, and SS in different tables, or maybe different databases.
|
||||
* OS: OperationStore holds all the operations. An operation is a js object with a fixed number of name fields.
|
||||
* DS: DeleteStore holds the information about which operations are deleted and which operations were garbage collected (no longer available in the OS).
|
||||
* SS: StateSet holds the current state of the OS. SS.getState(username) refers to the amount of operations that were received by that respective user.
|
||||
* Op: Operation defines an action on a shared type. But it is also the format in which we store the model of a type. This is why it is also called a Struct/Structure.
|
||||
* Type and Structure: We crearly distinguish between type and structure. Short explanation: A type (e.g. Strings, Numbers) have a number of functions that you can apply on them. (+) is well defined on both of them. They are *modeled* by a structure - the functions really change the structure of a type. Types can be implemented differently but still provide the same functionality. In Yjs, almost all types are realized as a doubly linked list (on which Yjs can provide eventual convergence)
|
||||
*
|
@ -3,6 +3,9 @@
|
||||
|
||||
/*
|
||||
Partial definition of a transaction
|
||||
|
||||
A transaction provides all the the async functionality on a database.
|
||||
|
||||
By convention, a transaction has the following properties:
|
||||
* ss for StateSet
|
||||
* os for OperationStore
|
||||
@ -75,6 +78,10 @@ class AbstractTransaction {
|
||||
constructor (store) {
|
||||
this.store = store
|
||||
}
|
||||
/*
|
||||
Get a type based on the id of its model.
|
||||
If it does not exist yes, create it.
|
||||
*/
|
||||
* getType (id) {
|
||||
var sid = JSON.stringify(id)
|
||||
var t = this.store.initializedTypes[sid]
|
||||
@ -87,12 +94,11 @@ class AbstractTransaction {
|
||||
}
|
||||
return t
|
||||
}
|
||||
* createType (model) {
|
||||
var sid = JSON.stringify(model.id)
|
||||
var t = yield* Y[model.type].initType.call(this, this.store, model)
|
||||
this.store.initializedTypes[sid] = t
|
||||
return t
|
||||
}
|
||||
/*
|
||||
Apply operations that this user created (no remote ones!)
|
||||
* does not check for Struct.*.requiredOps()
|
||||
* also broadcasts it through the connector
|
||||
*/
|
||||
* applyCreatedOperations (ops) {
|
||||
var send = []
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
@ -108,24 +114,13 @@ class AbstractTransaction {
|
||||
}
|
||||
}
|
||||
/*
|
||||
Delete an operation from the OS, and add it to the GC, if necessary.
|
||||
|
||||
Rulez:
|
||||
* The most left element in a list must not be deleted.
|
||||
=> There is at least one element in the list
|
||||
* When an operation o is deleted, then it checks if its right operation
|
||||
can be gc'd (iff it's deleted)
|
||||
Mark an operation as deleted, and add it to the GC, if possible.
|
||||
*/
|
||||
* deleteOperation (targetId) {
|
||||
var target = yield* this.getOperation(targetId)
|
||||
|
||||
if (target == null || !target.deleted) {
|
||||
this.ds.markDeleted(targetId)
|
||||
var state = yield* this.getState(targetId[0])
|
||||
if (state.clock === targetId[1]) {
|
||||
yield* this.checkDeleteStoreForState(state)
|
||||
yield* this.setState(state)
|
||||
}
|
||||
}
|
||||
|
||||
if (target != null && target.gc == null) {
|
||||
@ -143,23 +138,16 @@ class AbstractTransaction {
|
||||
var left = target.left != null ? yield* this.getOperation(target.left) : null
|
||||
var right = target.right != null ? yield* this.getOperation(target.right) : null
|
||||
|
||||
this.store.addToGarbageCollector(target, left, right)
|
||||
this.store.addToGarbageCollector(target, left)
|
||||
|
||||
// set here because it was deleted and/or gc'd
|
||||
yield* this.setOperation(target)
|
||||
|
||||
if (
|
||||
left != null &&
|
||||
left.left != null &&
|
||||
this.store.addToGarbageCollector(left, yield* this.getOperation(left.left), target)
|
||||
) {
|
||||
yield* this.setOperation(left)
|
||||
}
|
||||
|
||||
// check if it is possible to add right to the gc (this delete can't be responsible for left being gc'd)
|
||||
if (
|
||||
right != null &&
|
||||
right.right != null &&
|
||||
this.store.addToGarbageCollector(right, target, yield* this.getOperation(right.right))
|
||||
this.store.addToGarbageCollector(right, target)
|
||||
) {
|
||||
yield* this.setOperation(right)
|
||||
}
|
||||
@ -176,31 +164,45 @@ class AbstractTransaction {
|
||||
yield* this.deleteOperation(id)
|
||||
o = yield* this.getOperation(id)
|
||||
}
|
||||
|
||||
// check to increase the state of the respective user
|
||||
var state = yield* this.getState(id[0])
|
||||
if (state.clock === id[1]) {
|
||||
// also check if more expected operations were gc'd
|
||||
yield* this.checkDeleteStoreForState(state)
|
||||
// then set the state
|
||||
yield* this.setState(state)
|
||||
}
|
||||
|
||||
// remove gc'd op from the left op, if it exists
|
||||
if (o.left != null) {
|
||||
var left = yield* this.getOperation(o.left)
|
||||
left.right = o.right
|
||||
yield* this.setOperation(left)
|
||||
}
|
||||
// remove gc'd op from the right op, if it exists
|
||||
if (o.right != null) {
|
||||
var right = yield* this.getOperation(o.right)
|
||||
right.left = o.left
|
||||
yield* this.setOperation(right)
|
||||
}
|
||||
// remove gc'd op from parent, if it exists
|
||||
var parent = yield* this.getOperation(o.parent)
|
||||
var setParent = false
|
||||
var setParent = false // whether to save parent to the os
|
||||
if (Y.utils.compareIds(parent.start, o.id)) {
|
||||
// gc'd op is the start
|
||||
setParent = true
|
||||
parent.start = o.right
|
||||
}
|
||||
if (Y.utils.compareIds(parent.end, o.id)) {
|
||||
// gc'd op is the end
|
||||
setParent = true
|
||||
parent.end = o.left
|
||||
}
|
||||
if (setParent) {
|
||||
yield* this.setOperation(parent)
|
||||
}
|
||||
yield* this.removeOperation(o.id)
|
||||
yield* this.removeOperation(o.id) // actually remove it from the os
|
||||
yield* this.ds.markGarbageCollected(o.id)
|
||||
}
|
||||
}
|
||||
@ -272,10 +274,9 @@ class AbstractOperationStore {
|
||||
var os = this.os
|
||||
var self = this
|
||||
os.iterate(null, null, function (op) {
|
||||
if (op.deleted && op.left != null && op.right != null) {
|
||||
if (op.deleted && op.left != null) {
|
||||
var left = os.find(op.left)
|
||||
var right = os.find(op.right)
|
||||
self.addToGarbageCollector(op, left, right)
|
||||
self.addToGarbageCollector(op, left)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -283,25 +284,21 @@ class AbstractOperationStore {
|
||||
Try to add to GC.
|
||||
|
||||
TODO: rename this function
|
||||
|
||||
Only gc when
|
||||
* creator of op is online
|
||||
* left & right defined and both are from the same creator as op
|
||||
|
||||
|
||||
Rulez:
|
||||
* Only gc if this user is online
|
||||
* The most left element in a list must not be gc'd.
|
||||
=> There is at least one element in the list
|
||||
|
||||
returns true iff op was added to GC
|
||||
*/
|
||||
addToGarbageCollector (op, left, right) {
|
||||
addToGarbageCollector (op, left) {
|
||||
if (
|
||||
op.gc == null &&
|
||||
op.deleted === true &&
|
||||
this.y.connector.isSynced &&
|
||||
// (this.y.connector.connections[op.id[0]] != null || op.id[0] === this.y.connector.userId) &&
|
||||
left != null &&
|
||||
right != null &&
|
||||
left.deleted &&
|
||||
right.deleted &&
|
||||
left.id[0] === op.id[0] &&
|
||||
right.id[0] === op.id[0]
|
||||
) {
|
||||
op.gc = true
|
||||
this.gc1.push(op.id)
|
||||
@ -343,23 +340,25 @@ class AbstractOperationStore {
|
||||
}
|
||||
return [this.userId, this.opClock++]
|
||||
}
|
||||
/*
|
||||
Apply a list of operations.
|
||||
|
||||
* get a transaction
|
||||
* check whether all Struct.*.requiredOps are in the OS
|
||||
* check if it is an expected op (otherwise wait for it)
|
||||
* check if was deleted, apply a delete operation after op was applied
|
||||
*/
|
||||
apply (ops) {
|
||||
for (var key in ops) {
|
||||
var o = ops[key]
|
||||
if (o.gc == null) { // TODO: why do i get the same op twice?
|
||||
if (o.deleted == null) {
|
||||
var required = Y.Struct[o.struct].requiredOps(o)
|
||||
this.whenOperationsExist(required, o)
|
||||
} else {
|
||||
throw new Error('Ops must not contain deleted field!')
|
||||
}
|
||||
} else {
|
||||
throw new Error("Must not receive gc'd ops!")
|
||||
}
|
||||
var required = Y.Struct[o.struct].requiredOps(o)
|
||||
this.whenOperationsExist(required, o)
|
||||
}
|
||||
}
|
||||
// op is executed as soon as every operation requested is available.
|
||||
// Note that Transaction can (and should) buffer requests.
|
||||
/*
|
||||
op is executed as soon as every operation requested is available.
|
||||
Note that Transaction can (and should) buffer requests.
|
||||
*/
|
||||
whenOperationsExist (ids, op) {
|
||||
if (ids.length > 0) {
|
||||
let listener = {
|
||||
@ -390,7 +389,7 @@ class AbstractOperationStore {
|
||||
this.listenersByIdRequestPending = true
|
||||
var store = this
|
||||
|
||||
this.requestTransaction(function *() {
|
||||
this.requestTransaction(function * () {
|
||||
var exeNow = store.listenersByIdExecuteNow
|
||||
store.listenersByIdExecuteNow = []
|
||||
|
||||
@ -421,6 +420,13 @@ class AbstractOperationStore {
|
||||
}
|
||||
})
|
||||
}
|
||||
/*
|
||||
Actually execute an operation, when all expected operations are available.
|
||||
If op is not yet expected, add it to the list of waiting operations.
|
||||
|
||||
This will also try to execute waiting operations
|
||||
(ops that were not expected yet), after it was applied
|
||||
*/
|
||||
* tryExecute (op) {
|
||||
if (op.struct === 'Delete') {
|
||||
yield* Y.Struct.Delete.execute.call(this, op)
|
||||
@ -439,6 +445,7 @@ class AbstractOperationStore {
|
||||
yield* this.addOperation(op)
|
||||
yield* this.store.operationAdded(this, op)
|
||||
|
||||
// Delete if DS says this is actually deleted
|
||||
if (this.store.ds.isDeleted(op.id)) {
|
||||
yield* Y.Struct['Delete'].execute.call(this, {struct: 'Delete', target: op.id})
|
||||
}
|
||||
@ -479,21 +486,5 @@ class AbstractOperationStore {
|
||||
yield* t._changed(transaction, Y.utils.copyObject(op))
|
||||
}
|
||||
}
|
||||
removeParentListener (id, f) {
|
||||
var ls = this.parentListeners[id]
|
||||
if (ls != null) {
|
||||
this.parentListeners[id] = ls.filter(function (g) {
|
||||
return (f !== g)
|
||||
})
|
||||
}
|
||||
}
|
||||
addParentListener (id, f) {
|
||||
var ls = this.parentListeners[JSON.stringify(id)]
|
||||
if (ls == null) {
|
||||
ls = []
|
||||
this.parentListeners[JSON.stringify(id)] = ls
|
||||
}
|
||||
ls.push(f)
|
||||
}
|
||||
}
|
||||
Y.AbstractOperationStore = AbstractOperationStore
|
||||
|
@ -122,7 +122,7 @@ Y.IndexedDB = (function () { // eslint-disable-line
|
||||
}
|
||||
})()
|
||||
|
||||
function handleTransactions (t) { // eslint-disable-line no-unused-vars
|
||||
function handleTransactions (t) {
|
||||
var request = t.value
|
||||
if (t.done) {
|
||||
return
|
||||
|
@ -167,15 +167,16 @@
|
||||
Y.Array = new Y.utils.CustomType({
|
||||
class: YArray,
|
||||
createType: function * YArrayCreator () {
|
||||
var modelid = this.store.getNextOpId()
|
||||
var model = {
|
||||
struct: 'List',
|
||||
type: 'Array',
|
||||
start: null,
|
||||
end: null,
|
||||
id: this.store.getNextOpId()
|
||||
id: modelid
|
||||
}
|
||||
yield* this.applyCreatedOperations([model])
|
||||
return yield* this.createType(model)
|
||||
return modelid
|
||||
},
|
||||
initType: function * YArrayInitializer (os, model) {
|
||||
var valArray = []
|
||||
|
@ -134,8 +134,9 @@
|
||||
if (value instanceof Y.utils.CustomType) {
|
||||
// construct a new type
|
||||
this.os.requestTransaction(function *() {
|
||||
var type = yield* value.createType.call(this)
|
||||
insert.opContent = type._model
|
||||
var typeid = yield* value.createType.call(this)
|
||||
var type = yield* this.getType(typeid)
|
||||
insert.opContent = typeid
|
||||
insert.id = this.store.getNextOpId()
|
||||
yield* this.applyCreatedOperations([insert])
|
||||
resolve(type)
|
||||
@ -212,14 +213,15 @@
|
||||
Y.Map = new Y.utils.CustomType({
|
||||
class: YMap,
|
||||
createType: function * YMapCreator () {
|
||||
var modelid = this.store.getNextOpId()
|
||||
var model = {
|
||||
map: {},
|
||||
struct: 'Map',
|
||||
type: 'Map',
|
||||
id: this.store.getNextOpId()
|
||||
id: modelid
|
||||
}
|
||||
yield* this.applyCreatedOperations([model])
|
||||
return yield* this.createType(model)
|
||||
return modelid
|
||||
},
|
||||
initType: function * YMapInitializer (os, model) { // eslint-disable-line
|
||||
return new YMap(os, model)
|
||||
|
@ -267,15 +267,16 @@
|
||||
Y.TextBind = new Y.utils.CustomType({
|
||||
class: YTextBind,
|
||||
createType: function * YTextBindCreator () {
|
||||
var modelid = this.store.getNextOpId()
|
||||
var model = {
|
||||
start: null,
|
||||
end: null,
|
||||
struct: 'List',
|
||||
type: 'TextBind',
|
||||
id: this.store.getNextOpId()
|
||||
id: modelid
|
||||
}
|
||||
yield* this.applyCreatedOperations([model])
|
||||
return yield* this.createType(model)
|
||||
return modelid
|
||||
},
|
||||
initType: function * YTextBindInitializer (os, model) {
|
||||
var valArray = []
|
||||
|
Loading…
x
Reference in New Issue
Block a user