code refactoring, and documentation

This commit is contained in:
Kevin Jahns 2015-09-29 13:59:38 +02:00
parent 638c575dfc
commit 8745fd64ca
11 changed files with 116 additions and 117 deletions

View File

@ -1,12 +1,8 @@
language: node_js language: node_js
before_install: before_install:
- "npm install -g bower" - "npm install -g bower"
- "bower install"
node_js: node_js:
- "0.12" - "0.12"
- "0.11"
- "0.10"
branches: branches:
only: only:
- master - master
- 0.6

BIN
README.md

Binary file not shown.

View File

@ -28,8 +28,8 @@
Specify which specs to use! Specify which specs to use!
Commands: Commands:
- build - build:deploy
Build this library Build this library for deployment (es6->es5, minified)
- dev:browser - dev:browser
Watch the ./src directory. Watch the ./src directory.
Builds the library on changes. Builds the library on changes.

View File

@ -130,7 +130,7 @@ class AbstractConnector {
} }
send (uid, message) { send (uid, message) {
if (this.debug) { 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 return
} }
if (this.debug) { 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') { if (m.type === 'sync step 1') {
// TODO: make transaction, stream the ops // TODO: make transaction, stream the ops
@ -212,17 +212,19 @@ class AbstractConnector {
this.y.db.apply(m.ops) 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 Currently, the HB encodes operations as JSON. For the moment I want to keep it
// too much overhead. Y is very likely to get changed a lot in the future 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. 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.
// 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 does not support primitive values as array elements
expects an ltx (less than xml) object
*/
parseMessageFromXml (m) { parseMessageFromXml (m) {
function parseArray (node) { function parseArray (node) {
for (var n of node.children) { for (var n of node.children) {
@ -256,14 +258,16 @@ class AbstractConnector {
} }
parseObject(m) parseObject(m)
} }
// encode message in xml /*
// we use string because Strophe only accepts an "xml-string".. encode message in xml
// So {a:4,b:{c:5}} will look like we use string because Strophe only accepts an "xml-string"..
// <y a="4"> So {a:4,b:{c:5}} will look like
// <b c="5"></b> <y a="4">
// </y> <b c="5"></b>
// m - ltx element </y>
// json - Object m - ltx element
json - Object
*/
encodeMessageToXml (msg, obj) { encodeMessageToXml (msg, obj) {
// attributes is optional // attributes is optional
function encodeObject (m, json) { function encodeObject (m, json) {

12
src/Notes.md Normal file
View 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)
*

View File

@ -3,6 +3,9 @@
/* /*
Partial definition of a transaction Partial definition of a transaction
A transaction provides all the the async functionality on a database.
By convention, a transaction has the following properties: By convention, a transaction has the following properties:
* ss for StateSet * ss for StateSet
* os for OperationStore * os for OperationStore
@ -75,6 +78,10 @@ class AbstractTransaction {
constructor (store) { constructor (store) {
this.store = store this.store = store
} }
/*
Get a type based on the id of its model.
If it does not exist yes, create it.
*/
* getType (id) { * getType (id) {
var sid = JSON.stringify(id) var sid = JSON.stringify(id)
var t = this.store.initializedTypes[sid] var t = this.store.initializedTypes[sid]
@ -87,12 +94,11 @@ class AbstractTransaction {
} }
return t return t
} }
* createType (model) { /*
var sid = JSON.stringify(model.id) Apply operations that this user created (no remote ones!)
var t = yield* Y[model.type].initType.call(this, this.store, model) * does not check for Struct.*.requiredOps()
this.store.initializedTypes[sid] = t * also broadcasts it through the connector
return t */
}
* applyCreatedOperations (ops) { * applyCreatedOperations (ops) {
var send = [] var send = []
for (var i = 0; i < ops.length; i++) { 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. Mark an operation as deleted, and add it to the GC, if possible.
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)
*/ */
* deleteOperation (targetId) { * deleteOperation (targetId) {
var target = yield* this.getOperation(targetId) var target = yield* this.getOperation(targetId)
if (target == null || !target.deleted) { if (target == null || !target.deleted) {
this.ds.markDeleted(targetId) 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) { if (target != null && target.gc == null) {
@ -143,23 +138,16 @@ class AbstractTransaction {
var left = target.left != null ? yield* this.getOperation(target.left) : null var left = target.left != null ? yield* this.getOperation(target.left) : null
var right = target.right != null ? yield* this.getOperation(target.right) : 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 // set here because it was deleted and/or gc'd
yield* this.setOperation(target) yield* this.setOperation(target)
if ( // check if it is possible to add right to the gc (this delete can't be responsible for left being gc'd)
left != null &&
left.left != null &&
this.store.addToGarbageCollector(left, yield* this.getOperation(left.left), target)
) {
yield* this.setOperation(left)
}
if ( if (
right != null && right != null &&
right.right != null && right.right != null &&
this.store.addToGarbageCollector(right, target, yield* this.getOperation(right.right)) this.store.addToGarbageCollector(right, target)
) { ) {
yield* this.setOperation(right) yield* this.setOperation(right)
} }
@ -177,30 +165,44 @@ class AbstractTransaction {
o = yield* this.getOperation(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) { if (o.left != null) {
var left = yield* this.getOperation(o.left) var left = yield* this.getOperation(o.left)
left.right = o.right left.right = o.right
yield* this.setOperation(left) yield* this.setOperation(left)
} }
// remove gc'd op from the right op, if it exists
if (o.right != null) { if (o.right != null) {
var right = yield* this.getOperation(o.right) var right = yield* this.getOperation(o.right)
right.left = o.left right.left = o.left
yield* this.setOperation(right) yield* this.setOperation(right)
} }
// remove gc'd op from parent, if it exists
var parent = yield* this.getOperation(o.parent) 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)) { if (Y.utils.compareIds(parent.start, o.id)) {
// gc'd op is the start
setParent = true setParent = true
parent.start = o.right parent.start = o.right
} }
if (Y.utils.compareIds(parent.end, o.id)) { if (Y.utils.compareIds(parent.end, o.id)) {
// gc'd op is the end
setParent = true setParent = true
parent.end = o.left parent.end = o.left
} }
if (setParent) { if (setParent) {
yield* this.setOperation(parent) 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) yield* this.ds.markGarbageCollected(o.id)
} }
} }
@ -272,10 +274,9 @@ class AbstractOperationStore {
var os = this.os var os = this.os
var self = this var self = this
os.iterate(null, null, function (op) { 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 left = os.find(op.left)
var right = os.find(op.right) self.addToGarbageCollector(op, left)
self.addToGarbageCollector(op, left, right)
} }
}) })
} }
@ -284,24 +285,20 @@ class AbstractOperationStore {
TODO: rename this function TODO: rename this function
Only gc when Rulez:
* creator of op is online * Only gc if this user is online
* left & right defined and both are from the same creator as op * 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 returns true iff op was added to GC
*/ */
addToGarbageCollector (op, left, right) { addToGarbageCollector (op, left) {
if ( if (
op.gc == null && op.gc == null &&
op.deleted === true && op.deleted === true &&
this.y.connector.isSynced && this.y.connector.isSynced &&
// (this.y.connector.connections[op.id[0]] != null || op.id[0] === this.y.connector.userId) &&
left != null && left != null &&
right != null &&
left.deleted && left.deleted &&
right.deleted &&
left.id[0] === op.id[0] &&
right.id[0] === op.id[0]
) { ) {
op.gc = true op.gc = true
this.gc1.push(op.id) this.gc1.push(op.id)
@ -343,23 +340,25 @@ class AbstractOperationStore {
} }
return [this.userId, this.opClock++] 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) { apply (ops) {
for (var key in ops) { for (var key in ops) {
var o = ops[key] 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) var required = Y.Struct[o.struct].requiredOps(o)
this.whenOperationsExist(required, 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!")
} }
} }
} /*
// op is executed as soon as every operation requested is available. op is executed as soon as every operation requested is available.
// Note that Transaction can (and should) buffer requests. Note that Transaction can (and should) buffer requests.
*/
whenOperationsExist (ids, op) { whenOperationsExist (ids, op) {
if (ids.length > 0) { if (ids.length > 0) {
let listener = { let listener = {
@ -390,7 +389,7 @@ class AbstractOperationStore {
this.listenersByIdRequestPending = true this.listenersByIdRequestPending = true
var store = this var store = this
this.requestTransaction(function *() { this.requestTransaction(function * () {
var exeNow = store.listenersByIdExecuteNow var exeNow = store.listenersByIdExecuteNow
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) { * tryExecute (op) {
if (op.struct === 'Delete') { if (op.struct === 'Delete') {
yield* Y.Struct.Delete.execute.call(this, op) yield* Y.Struct.Delete.execute.call(this, op)
@ -439,6 +445,7 @@ class AbstractOperationStore {
yield* this.addOperation(op) yield* this.addOperation(op)
yield* this.store.operationAdded(this, op) yield* this.store.operationAdded(this, op)
// Delete if DS says this is actually deleted
if (this.store.ds.isDeleted(op.id)) { if (this.store.ds.isDeleted(op.id)) {
yield* Y.Struct['Delete'].execute.call(this, {struct: 'Delete', target: 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)) 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 Y.AbstractOperationStore = AbstractOperationStore

View File

@ -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 var request = t.value
if (t.done) { if (t.done) {
return return

View File

@ -167,15 +167,16 @@
Y.Array = new Y.utils.CustomType({ Y.Array = new Y.utils.CustomType({
class: YArray, class: YArray,
createType: function * YArrayCreator () { createType: function * YArrayCreator () {
var modelid = this.store.getNextOpId()
var model = { var model = {
struct: 'List', struct: 'List',
type: 'Array', type: 'Array',
start: null, start: null,
end: null, end: null,
id: this.store.getNextOpId() id: modelid
} }
yield* this.applyCreatedOperations([model]) yield* this.applyCreatedOperations([model])
return yield* this.createType(model) return modelid
}, },
initType: function * YArrayInitializer (os, model) { initType: function * YArrayInitializer (os, model) {
var valArray = [] var valArray = []

View File

@ -134,8 +134,9 @@
if (value instanceof Y.utils.CustomType) { if (value instanceof Y.utils.CustomType) {
// construct a new type // construct a new type
this.os.requestTransaction(function *() { this.os.requestTransaction(function *() {
var type = yield* value.createType.call(this) var typeid = yield* value.createType.call(this)
insert.opContent = type._model var type = yield* this.getType(typeid)
insert.opContent = typeid
insert.id = this.store.getNextOpId() insert.id = this.store.getNextOpId()
yield* this.applyCreatedOperations([insert]) yield* this.applyCreatedOperations([insert])
resolve(type) resolve(type)
@ -212,14 +213,15 @@
Y.Map = new Y.utils.CustomType({ Y.Map = new Y.utils.CustomType({
class: YMap, class: YMap,
createType: function * YMapCreator () { createType: function * YMapCreator () {
var modelid = this.store.getNextOpId()
var model = { var model = {
map: {}, map: {},
struct: 'Map', struct: 'Map',
type: 'Map', type: 'Map',
id: this.store.getNextOpId() id: modelid
} }
yield* this.applyCreatedOperations([model]) yield* this.applyCreatedOperations([model])
return yield* this.createType(model) return modelid
}, },
initType: function * YMapInitializer (os, model) { // eslint-disable-line initType: function * YMapInitializer (os, model) { // eslint-disable-line
return new YMap(os, model) return new YMap(os, model)

View File

@ -267,15 +267,16 @@
Y.TextBind = new Y.utils.CustomType({ Y.TextBind = new Y.utils.CustomType({
class: YTextBind, class: YTextBind,
createType: function * YTextBindCreator () { createType: function * YTextBindCreator () {
var modelid = this.store.getNextOpId()
var model = { var model = {
start: null, start: null,
end: null, end: null,
struct: 'List', struct: 'List',
type: 'TextBind', type: 'TextBind',
id: this.store.getNextOpId() id: modelid
} }
yield* this.applyCreatedOperations([model]) yield* this.applyCreatedOperations([model])
return yield* this.createType(model) return modelid
}, },
initType: function * YTextBindInitializer (os, model) { initType: function * YTextBindInitializer (os, model) {
var valArray = [] var valArray = []

View File

@ -37,14 +37,6 @@ class YConfig {
} }
reconnect () { reconnect () {
this.connector.reconnect() this.connector.reconnect()
/* TODO: maybe do this..
Promise.all([
this.db.garbageCollect(),
this.db.garbageCollect()
]).then(() => {
this.connector.reconnect()
})
*/
} }
destroy () { destroy () {
this.connector.disconnect() this.connector.disconnect()