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.
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 () {

View File

@ -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 () {

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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()