Skip to content
Snippets Groups Projects
Commit 8745fd64 authored by Kevin Jahns's avatar Kevin Jahns
Browse files

code refactoring, and documentation

parent 638c575d
No related branches found
No related tags found
No related merge requests found
language: node_js
before_install:
- "npm install -g bower"
- "bower install"
node_js:
- "0.12"
- "0.11"
- "0.10"
branches:
only:
- master
- 0.6
No preview for this file type
......@@ -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) {
......
# 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)
*
\ No newline at end of file
......@@ -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 = []
......
......@@ -37,14 +37,6 @@ class YConfig {
}
reconnect () {
this.connector.reconnect()
/* TODO: maybe do this..
Promise.all([
this.db.garbageCollect(),
this.db.garbageCollect()
]).then(() => {
this.connector.reconnect()
})
*/
}
destroy () {
this.connector.disconnect()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment