added offline editing demo 🌟
This commit is contained in:
parent
04139d3b7e
commit
29f3f3f722
12
Examples/OfflineEditing/index.html
Normal file
12
Examples/OfflineEditing/index.html
Normal 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>
|
50
Examples/OfflineEditing/index.js
Normal file
50
Examples/OfflineEditing/index.js
Normal 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'
|
||||
}
|
||||
}
|
||||
})
|
@ -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 () {
|
||||
|
@ -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 () {
|
||||
|
@ -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
|
||||
|
@ -36,7 +36,7 @@ var Struct = {
|
||||
return [] // [op.target]
|
||||
},
|
||||
execute: function * (op) {
|
||||
yield* this.deleteOperation(op.target)
|
||||
return yield* this.deleteOperation(op.target)
|
||||
}
|
||||
},
|
||||
Insert: {
|
||||
|
@ -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)
|
||||
|
@ -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!')
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user