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.
|
// 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 () {
|
||||||
|
@ -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 () {
|
||||||
|
@ -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
|
||||||
|
@ -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: {
|
||||||
|
@ -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)
|
||||||
|
@ -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!')
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
2
src/y.js
2
src/y.js
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user