added offline editing demo 🌟

This commit is contained in:
Kevin Jahns 2015-10-18 03:07:34 +02:00
parent 04139d3b7e
commit 29f3f3f722
11 changed files with 165 additions and 48 deletions

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<body>
<button id="button">Disconnect</button>
<h1 id="contenteditable" contentEditable></h1>
<textarea style="width:80%;" rows=40 id="textfield"></textarea>
<script src="../../node_modules/simplewebrtc/simplewebrtc.bundle.js"></script>
<script src="../../y.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@ -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'
}
}
})

View File

@ -36,6 +36,8 @@ class AbstractDatabase {
// wont be kept in memory. // wont be kept in memory.
this.initializedTypes = {} this.initializedTypes = {}
this.whenUserIdSetListener = null this.whenUserIdSetListener = null
this.waitingTransactions = []
this.transactionInProgress = false
if (typeof YConcurrency_TestingMode !== 'undefined') { if (typeof YConcurrency_TestingMode !== 'undefined') {
this.executeOrder = [] this.executeOrder = []
} }
@ -46,7 +48,7 @@ class AbstractDatabase {
function garbageCollect () { function garbageCollect () {
return new Promise((resolve) => { return new Promise((resolve) => {
os.requestTransaction(function * () { os.requestTransaction(function * () {
if (os.y.connector.isSynced) { if (os.y.connector != null && os.y.connector.isSynced) {
for (var i in os.gc2) { for (var i in os.gc2) {
var oid = os.gc2[i] var oid = os.gc2[i]
yield* this.garbageCollectOperation(oid) yield* this.garbageCollectOperation(oid)
@ -65,8 +67,6 @@ class AbstractDatabase {
if (this.gcTimeout > 0) { if (this.gcTimeout > 0) {
garbageCollect() garbageCollect()
} }
this.waitingTransactions = []
this.transactionInProgress = false
} }
addToDebug () { addToDebug () {
if (typeof YConcurrency_TestingMode !== 'undefined') { if (typeof YConcurrency_TestingMode !== 'undefined') {
@ -252,47 +252,68 @@ class AbstractDatabase {
this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(op), ')') this.store.addToDebug('yield* this.store.tryExecute.call(this, ', JSON.stringify(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)
yield* this.store.operationAdded(this, op)
} else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) { } else if ((yield* this.getOperation(op.id)) == null && !(yield* this.isGarbageCollected(op.id))) {
yield* Y.Struct[op.struct].execute.call(this, op) yield* Y.Struct[op.struct].execute.call(this, op)
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 (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 // called by a transaction when an operation is added
* operationAdded (transaction, op) { * operationAdded (transaction, op) {
// increase SS if (op.struct === 'Delete') {
var o = op var target = yield* transaction.getOperation(op.target)
var state = yield* transaction.getState(op.id[0]) if (target != null) {
while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) { var type = transaction.store.initializedTypes[JSON.stringify(target.parent)]
// either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS if (type != null) {
state.clock++ yield* type._changed(transaction, {
yield* transaction.checkDeleteStoreForState(state) struct: 'Delete',
o = yield* transaction.os.findNext(o.id) target: op.target
} })
yield* transaction.setState(state) }
}
// notify whenOperation listeners (by id) } else {
var sid = JSON.stringify(op.id) // increase SS
var l = this.listenersById[sid] var o = op
delete this.listenersById[sid] var state = yield* transaction.getState(op.id[0])
while (o != null && o.id[1] === state.clock && op.id[0] === o.id[0]) {
if (l != null) { // either its a new operation (1. case), or it is an operation that was deleted, but is not yet in the OS
for (var key in l) { state.clock++
var listener = l[key] yield* transaction.checkDeleteStoreForState(state)
if (--listener.missing === 0) { o = yield* transaction.os.findNext(o.id)
this.whenOperationsExist([], listener.op) }
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 () { getNextRequest () {

View File

@ -1,7 +1,7 @@
/* global Y, async, databases */ /* global Y, async, databases */
/* eslint-env browser,jasmine,console */ /* eslint-env browser,jasmine,console */
for (var database of databases) { for (let database of databases) {
describe(`Database (${database})`, function () { describe(`Database (${database})`, function () {
var store var store
describe('DeleteStore', function () { describe('DeleteStore', function () {

View File

@ -83,6 +83,29 @@ Y.IndexedDB = (function () {
yield this.ss.store.clear() 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) { transact (makeGen) {
var transaction = this.db != null ? new Transaction(this) : null var transaction = this.db != null ? new Transaction(this) : null

View File

@ -36,7 +36,7 @@ var Struct = {
return [] // [op.target] return [] // [op.target]
}, },
execute: function * (op) { execute: function * (op) {
yield* this.deleteOperation(op.target) return yield* this.deleteOperation(op.target)
} }
}, },
Insert: { Insert: {

View File

@ -132,6 +132,7 @@ class Transaction {
*/ */
* deleteOperation (targetId, preventCallType) { * deleteOperation (targetId, preventCallType) {
var target = yield* this.getOperation(targetId) var target = yield* this.getOperation(targetId)
var callType = false
if (target == null || !target.deleted) { if (target == null || !target.deleted) {
yield* this.markDeleted(targetId) yield* this.markDeleted(targetId)
@ -139,8 +140,10 @@ class Transaction {
if (target != null && target.gc == null) { if (target != null && target.gc == null) {
if (!target.deleted) { if (!target.deleted) {
callType = true
// set deleted & notify type // set deleted & notify type
target.deleted = true target.deleted = true
/*
if (!preventCallType) { if (!preventCallType) {
var type = this.store.initializedTypes[JSON.stringify(target.parent)] var type = this.store.initializedTypes[JSON.stringify(target.parent)]
if (type != null) { if (type != null) {
@ -150,6 +153,7 @@ class Transaction {
}) })
} }
} }
*/
// delete containing lists // delete containing lists
if (target.start != null) { if (target.start != null) {
// TODO: don't do it like this .. -.- // TODO: don't do it like this .. -.-
@ -187,6 +191,7 @@ class Transaction {
) { ) {
yield* this.setOperation(right) yield* this.setOperation(right)
} }
return callType
} }
} }
/* /*
@ -468,7 +473,11 @@ class Transaction {
var del = deletions[i] var del = deletions[i]
var id = [del[0], del[1]] var id = [del[0], del[1]]
// always try to delete.. // 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]) { if (del[2]) {
// gc // gc
yield* this.garbageCollectOperation(id) yield* this.garbageCollectOperation(id)

View File

@ -37,14 +37,16 @@
}) })
} else if (op.struct === 'Delete') { } else if (op.struct === 'Delete') {
let pos = this.idArray.indexOf(JSON.stringify(op.target)) let pos = this.idArray.indexOf(JSON.stringify(op.target))
this.idArray.splice(pos, 1) if (pos >= 0) {
this.valArray.splice(pos, 1) this.idArray.splice(pos, 1)
userEvents.push({ this.valArray.splice(pos, 1)
type: 'delete', userEvents.push({
object: this, type: 'delete',
index: pos, object: this,
length: 1 index: pos,
}) length: 1
})
}
} else { } else {
throw new Error('Unexpected struct!') throw new Error('Unexpected struct!')
} }

View File

@ -4,7 +4,7 @@
var numberOfYArrayTests = 50 var numberOfYArrayTests = 50
var repeatArrayTests = 2 var repeatArrayTests = 2
for (var database of databases) { for (let database of databases) {
describe(`Array Type (DB: ${database})`, function () { describe(`Array Type (DB: ${database})`, function () {
var y1, y2, y3, yconfig1, yconfig2, yconfig3, flushAll var y1, y2, y3, yconfig1, yconfig2, yconfig3, flushAll

View File

@ -4,7 +4,7 @@
var numberOfYMapTests = 40 var numberOfYMapTests = 40
var repeatMapTeasts = 2 var repeatMapTeasts = 2
for (var database of databases) { for (let database of databases) {
describe(`Map Type (DB: ${database})`, function () { describe(`Map Type (DB: ${database})`, function () {
var y1, y2, y3, y4, flushAll var y1, y2, y3, y4, flushAll

View File

@ -23,7 +23,7 @@ class YConfig {
type: 'Map', type: 'Map',
map: {} map: {}
} }
yield* this.addOperation(model) yield* this.store.tryExecute.call(this, model)
var root = yield* this.getType(model.id) var root = yield* this.getType(model.id)
this.store.y.root = root this.store.y.root = root
callback() callback()