diff --git a/Examples/OfflineEditing/index.html b/Examples/OfflineEditing/index.html new file mode 100644 index 00000000..9bfd4525 --- /dev/null +++ b/Examples/OfflineEditing/index.html @@ -0,0 +1,12 @@ + + + + +

+ + + + + + + diff --git a/Examples/OfflineEditing/index.js b/Examples/OfflineEditing/index.js new file mode 100644 index 00000000..78a347b7 --- /dev/null +++ b/Examples/OfflineEditing/index.js @@ -0,0 +1,50 @@ +/* global Y */ + +// create a shared object. This function call will return a promise! +Y({ + db: { + name: 'IndexedDB', + namespace: 'offlineEditingDemo' + }, + connector: { + name: 'WebRTC', + room: 'offlineEditingDemo', + debug: true + } +}).then(function (yconfig) { + // yconfig holds all the information about the shared object + window.yconfig = yconfig + // yconfig.root holds the shared element + window.y = yconfig.root + + // now we bind the textarea and the contenteditable h1 element + // to a shared element + var textarea = document.getElementById('textfield') + var contenteditable = document.getElementById('contenteditable') + yconfig.root.observePath(['text'], function (text) { + // every time the 'text' property of the yconfig.root changes, + // this function is called. Then we bind it to the html elements + if (text != null) { + // when the text property is deleted, text may be undefined! + // This is why we have to check if text exists.. + text.bind(textarea) + text.bind(contenteditable) + } + }) + // create a shared TextBind + var textpromise = yconfig.root.get('text') + if (textpromise == null) { + yconfig.root.set('text', Y.TextBind) + } + // We also provide a button for disconnecting/reconnecting the shared element + var button = document.querySelector('#button') + button.onclick = function () { + if (button.innerText === 'Disconnect') { + yconfig.disconnect() + button.innerText = 'Reconnect' + } else { + yconfig.reconnect() + button.innerText = 'Disconnect' + } + } +}) diff --git a/src/Database.js b/src/Database.js index cc6df6be..c5e3e194 100644 --- a/src/Database.js +++ b/src/Database.js @@ -36,6 +36,8 @@ class AbstractDatabase { // wont be kept in memory. this.initializedTypes = {} this.whenUserIdSetListener = null + this.waitingTransactions = [] + this.transactionInProgress = false if (typeof YConcurrency_TestingMode !== 'undefined') { this.executeOrder = [] } @@ -46,7 +48,7 @@ class AbstractDatabase { function garbageCollect () { return new Promise((resolve) => { os.requestTransaction(function * () { - if (os.y.connector.isSynced) { + if (os.y.connector != null && os.y.connector.isSynced) { for (var i in os.gc2) { var oid = os.gc2[i] yield* this.garbageCollectOperation(oid) @@ -65,8 +67,6 @@ class AbstractDatabase { if (this.gcTimeout > 0) { garbageCollect() } - this.waitingTransactions = [] - this.transactionInProgress = false } addToDebug () { if (typeof YConcurrency_TestingMode !== 'undefined') { @@ -252,47 +252,68 @@ class AbstractDatabase { this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')') if (op.struct === 'Delete') { yield* Y.Struct.Delete.execute.call(this, op) + yield* this.store.operationAdded(this, op) } else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) { yield* Y.Struct[op.struct].execute.call(this, op) yield* this.addOperation(op) yield* this.store.operationAdded(this, op) - - // Delete if DS says this is actually deleted - if (yield* this.isDeleted(op.id)) { - yield* Y.Struct['Delete'].execute.call(this, {struct: 'Delete', target: op.id}) - } } } // called by a transaction when an operation is added * operationAdded (transaction, op) { - // increase SS - var o = op - var state = yield* transaction.getState(op.id[0]) - while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) { - // either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS - state.clock++ - yield* transaction.checkDeleteStoreForState(state) - o = yield* transaction.os.findNext(o.id) - } - yield* transaction.setState(state) - - // notify whenOperation listeners (by id) - var sid = JSON.stringify(op.id) - var l = this.listenersById[sid] - delete this.listenersById[sid] - - if (l != null) { - for (var key in l) { - var listener = l[key] - if (--listener.missing === 0) { - this.whenOperationsExist([], listener.op) + if (op.struct === 'Delete') { + var target = yield* transaction.getOperation(op.target) + if (target != null) { + var type = transaction.store.initializedTypes[JSON.stringify(target.parent)] + if (type != null) { + yield* type._changed(transaction, { + struct: 'Delete', + target: op.target + }) + } + } + } else { + // increase SS + var o = op + var state = yield* transaction.getState(op.id[0]) + while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) { + // either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS + state.clock++ + yield* transaction.checkDeleteStoreForState(state) + o = yield* transaction.os.findNext(o.id) + } + yield* transaction.setState(state) + + // notify whenOperation listeners (by id) + var sid = JSON.stringify(op.id) + var l = this.listenersById[sid] + delete this.listenersById[sid] + + if (l != null) { + for (var key in l) { + var listener = l[key] + if (--listener.missing === 0) { + this.whenOperationsExist([], listener.op) + } + } + } + var t = this.initializedTypes[JSON.stringify(op.parent)] + // notify parent, if it has been initialized as a custom type + if (t != null) { + yield* t._changed(transaction, Y.utils.copyObject(op)) + } + + // Delete if DS says this is actually deleted + if (!op.deleted && (yield* transaction.isDeleted(op.id))) { + var delop = { + struct: 'Delete', + target: op.id + } + yield* Y.Struct['Delete'].execute.call(transaction, delop) + if (t != null) { + yield* t._changed(transaction, delop) } } - } - // notify parent, if it has been initialized as a custom type - var t = this.initializedTypes[JSON.stringify(op.parent)] - if (t != null && !op.deleted) { - yield* t._changed(transaction, Y.utils.copyObject(op)) } } getNextRequest () { diff --git a/src/Database.spec.js b/src/Database.spec.js index 7a64e313..2305bf21 100644 --- a/src/Database.spec.js +++ b/src/Database.spec.js @@ -1,7 +1,7 @@ /* global Y, async, databases */ /* eslint-env browser,jasmine,console */ -for (var database of databases) { +for (let database of databases) { describe(`Database (${database})`, function () { var store describe('DeleteStore', function () { diff --git a/src/Databases/IndexedDB.js b/src/Databases/IndexedDB.js index 87331765..16823321 100644 --- a/src/Databases/IndexedDB.js +++ b/src/Databases/IndexedDB.js @@ -83,6 +83,29 @@ Y.IndexedDB = (function () { yield this.ss.store.clear() }) } + var operationsToAdd = [] + window.addEventListener('storage', function (event) { + if (event.key === '__YJS__' + store.namespace) { + operationsToAdd.push(event.newValue) + if (operationsToAdd.length === 1) { + store.requestTransaction(function * () { + var add = operationsToAdd + operationsToAdd = [] + for (var i in add) { + // don't call the localStorage event twice.. + var op = yield* this.getOperation(JSON.parse(add[i]).id) + yield* this.store.operationAdded(this, op, true) + } + }) + } + } + }, false) + } + * operationAdded (transaction, op, noAdd) { + yield* super.operationAdded(transaction, op) + if (!noAdd) { + window.localStorage['__YJS__' + this.namespace] = JSON.stringify(op) + } } transact (makeGen) { var transaction = this.db != null ? new Transaction(this) : null diff --git a/src/Struct.js b/src/Struct.js index 9f625417..fb1f72e1 100644 --- a/src/Struct.js +++ b/src/Struct.js @@ -36,7 +36,7 @@ var Struct = { return [] // [op.target] }, execute: function * (op) { - yield* this.deleteOperation(op.target) + return yield* this.deleteOperation(op.target) } }, Insert: { diff --git a/src/Transaction.js b/src/Transaction.js index def2ef66..8084777c 100644 --- a/src/Transaction.js +++ b/src/Transaction.js @@ -132,6 +132,7 @@ class Transaction { */ * deleteOperation (targetId, preventCallType) { var target = yield* this.getOperation(targetId) + var callType = false if (target == null || !target.deleted) { yield* this.markDeleted(targetId) @@ -139,8 +140,10 @@ class Transaction { if (target != null && target.gc == null) { if (!target.deleted) { + callType = true // set deleted & notify type target.deleted = true + /* if (!preventCallType) { var type = this.store.initializedTypes[JSON.stringify(target.parent)] if (type != null) { @@ -150,6 +153,7 @@ class Transaction { }) } } + */ // delete containing lists if (target.start != null) { // TODO: don't do it like this .. -.- @@ -187,6 +191,7 @@ class Transaction { ) { yield* this.setOperation(right) } + return callType } } /* @@ -468,7 +473,11 @@ class Transaction { var del = deletions[i] var id = [del[0], del[1]] // always try to delete.. - yield* this.deleteOperation(id) + var addOperation = yield* this.deleteOperation(id) + if (addOperation) { + // TODO:.. really .. here? You could prevent calling all these functions in operationAdded + yield* this.store.operationAdded(this, {struct: 'Delete', target: id}) + } if (del[2]) { // gc yield* this.garbageCollectOperation(id) diff --git a/src/Types/Array.js b/src/Types/Array.js index f2059343..4445fcf1 100644 --- a/src/Types/Array.js +++ b/src/Types/Array.js @@ -37,14 +37,16 @@ }) } else if (op.struct === 'Delete') { let pos = this.idArray.indexOf(JSON.stringify(op.target)) - this.idArray.splice(pos, 1) - this.valArray.splice(pos, 1) - userEvents.push({ - type: 'delete', - object: this, - index: pos, - length: 1 - }) + if (pos >= 0) { + this.idArray.splice(pos, 1) + this.valArray.splice(pos, 1) + userEvents.push({ + type: 'delete', + object: this, + index: pos, + length: 1 + }) + } } else { throw new Error('Unexpected struct!') } diff --git a/src/Types/Array.spec.js b/src/Types/Array.spec.js index 21fc75ea..bdb3d4fb 100644 --- a/src/Types/Array.spec.js +++ b/src/Types/Array.spec.js @@ -4,7 +4,7 @@ var numberOfYArrayTests = 50 var repeatArrayTests = 2 -for (var database of databases) { +for (let database of databases) { describe(`Array Type (DB: ${database})`, function () { var y1, y2, y3, yconfig1, yconfig2, yconfig3, flushAll diff --git a/src/Types/Map.spec.js b/src/Types/Map.spec.js index 4fd8e59a..a338fbf9 100644 --- a/src/Types/Map.spec.js +++ b/src/Types/Map.spec.js @@ -4,7 +4,7 @@ var numberOfYMapTests = 40 var repeatMapTeasts = 2 -for (var database of databases) { +for (let database of databases) { describe(`Map Type (DB: ${database})`, function () { var y1, y2, y3, y4, flushAll diff --git a/src/y.js b/src/y.js index f6bb96bf..5f5266d1 100644 --- a/src/y.js +++ b/src/y.js @@ -23,7 +23,7 @@ class YConfig { type: 'Map', map: {} } - yield* this.addOperation(model) + yield* this.store.tryExecute.call(this, model) var root = yield* this.getType(model.id) this.store.y.root = root callback()