Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47bcec8bc7 | ||
|
|
81c8504462 | ||
|
|
c926ce09f5 | ||
|
|
653a436b88 | ||
|
|
5ab60028ce | ||
|
|
428d825f41 | ||
|
|
b9f9c762eb | ||
|
|
80ab682b0a | ||
|
|
3f60690880 | ||
|
|
e2f93af86e | ||
|
|
76ebd3043d | ||
|
|
b958b72f1d |
19
Examples/Drawing/index.html
Normal file
19
Examples/Drawing/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<style>
|
||||
path {
|
||||
fill: none;
|
||||
stroke: blue;
|
||||
stroke-width: 1px;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
</style>
|
||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
88
Examples/Drawing/index.js
Normal file
88
Examples/Drawing/index.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/* globals Y, d3 */
|
||||
'strict mode'
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'drawing-example'
|
||||
// url: 'localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
drawing: 'Array'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yDrawing = y
|
||||
var drawing = y.share.drawing
|
||||
var renderPath = d3.svg.line()
|
||||
.x(function (d) { return d[0] })
|
||||
.y(function (d) { return d[1] })
|
||||
.interpolate('basis')
|
||||
|
||||
var svg = d3.select('#drawingCanvas')
|
||||
.call(d3.behavior.drag()
|
||||
.on('dragstart', dragstart)
|
||||
.on('drag', drag)
|
||||
.on('dragend', dragend))
|
||||
|
||||
// create line from a shared array object and update the line when the array changes
|
||||
function drawLine (yarray) {
|
||||
var line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
yarray.observe(function (event) {
|
||||
// we only implement insert events that are appended to the end of the array
|
||||
event.values.forEach(function (value) {
|
||||
line.datum().push(value)
|
||||
})
|
||||
line.attr('d', renderPath)
|
||||
})
|
||||
}
|
||||
// call drawLine every time an array is appended
|
||||
y.share.drawing.observe(function (event) {
|
||||
if (event.type === 'insert') {
|
||||
event.values().then(function (values) {
|
||||
values.forEach(drawLine)
|
||||
})
|
||||
} else {
|
||||
// just remove all elements (thats what we do anyway)
|
||||
svg.selectAll('path').remove()
|
||||
}
|
||||
})
|
||||
// draw all existing content
|
||||
for (var i = 0; i < drawing.length; i++) {
|
||||
drawing.get(i).then(drawLine)
|
||||
}
|
||||
|
||||
// clear canvas on request
|
||||
document.querySelector('#clearDrawingCanvas').onclick = function () {
|
||||
drawing.delete(0, drawing.length)
|
||||
}
|
||||
|
||||
var sharedLine = null
|
||||
function dragstart () {
|
||||
drawing.insert(drawing.length, [Y.Array])
|
||||
drawing.get(drawing.length - 1).then(function (array) {
|
||||
sharedLine = array
|
||||
})
|
||||
}
|
||||
|
||||
// After one dragged event is recognized, we ignore them for 33ms.
|
||||
var ignoreDrag = null
|
||||
function drag () {
|
||||
if (sharedLine != null && ignoreDrag == null) {
|
||||
ignoreDrag = window.setTimeout(function () {
|
||||
ignoreDrag = null
|
||||
}, 33)
|
||||
sharedLine.push([d3.mouse(this)])
|
||||
}
|
||||
}
|
||||
|
||||
function dragend () {
|
||||
sharedLine = null
|
||||
window.clearTimeout(ignoreDrag)
|
||||
ignoreDrag = null
|
||||
}
|
||||
})
|
||||
@@ -2,142 +2,24 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" />
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||
<style>
|
||||
#quill {
|
||||
#quill-container {
|
||||
border: 1px solid gray;
|
||||
box-shadow: 0px 0px 10px gray;
|
||||
}
|
||||
#toolbar {
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="quill">
|
||||
<!-- Create the toolbar container -->
|
||||
<div id="toolbar" class="toolbar">
|
||||
<span class="ql-format-group">
|
||||
<select title="Font" class="ql-font">
|
||||
<option value="sans-serif" selected="">Sans Serif</option>
|
||||
<option value="serif">Serif</option>
|
||||
<option value="monospace">Monospace</option>
|
||||
</select>
|
||||
<select title="Size" class="ql-size">
|
||||
<option value="10px">Small</option>
|
||||
<option value="13px" selected="">Normal</option>
|
||||
<option value="18px">Large</option>
|
||||
<option value="32px">Huge</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="ql-format-group">
|
||||
<span title="Bold" class="ql-format-button ql-bold"></span>
|
||||
<span class="ql-format-separator"></span>
|
||||
<span title="Italic" class="ql-format-button ql-italic"></span>
|
||||
<span class="ql-format-separator"></span>
|
||||
<span title="Underline" class="ql-format-button ql-underline"></span>
|
||||
<span class="ql-format-separator"></span>
|
||||
<span title="Strikethrough" class="ql-format-button ql-strike"></span>
|
||||
</span>
|
||||
<span class="ql-format-group">
|
||||
<select title="Text Color" class="ql-color">
|
||||
<option value="rgb(0, 0, 0)" label="rgb(0, 0, 0)" selected=""></option>
|
||||
<option value="rgb(230, 0, 0)" label="rgb(230, 0, 0)"></option>
|
||||
<option value="rgb(255, 153, 0)" label="rgb(255, 153, 0)"></option>
|
||||
<option value="rgb(255, 255, 0)" label="rgb(255, 255, 0)"></option>
|
||||
<option value="rgb(0, 138, 0)" label="rgb(0, 138, 0)"></option>
|
||||
<option value="rgb(0, 102, 204)" label="rgb(0, 102, 204)"></option>
|
||||
<option value="rgb(153, 51, 255)" label="rgb(153, 51, 255)"></option>
|
||||
<option value="rgb(255, 255, 255)" label="rgb(255, 255, 255)"></option>
|
||||
<option value="rgb(250, 204, 204)" label="rgb(250, 204, 204)"></option>
|
||||
<option value="rgb(255, 235, 204)" label="rgb(255, 235, 204)"></option>
|
||||
<option value="rgb(255, 255, 204)" label="rgb(255, 255, 204)"></option>
|
||||
<option value="rgb(204, 232, 204)" label="rgb(204, 232, 204)"></option>
|
||||
<option value="rgb(204, 224, 245)" label="rgb(204, 224, 245)"></option>
|
||||
<option value="rgb(235, 214, 255)" label="rgb(235, 214, 255)"></option>
|
||||
<option value="rgb(187, 187, 187)" label="rgb(187, 187, 187)"></option>
|
||||
<option value="rgb(240, 102, 102)" label="rgb(240, 102, 102)"></option>
|
||||
<option value="rgb(255, 194, 102)" label="rgb(255, 194, 102)"></option>
|
||||
<option value="rgb(255, 255, 102)" label="rgb(255, 255, 102)"></option>
|
||||
<option value="rgb(102, 185, 102)" label="rgb(102, 185, 102)"></option>
|
||||
<option value="rgb(102, 163, 224)" label="rgb(102, 163, 224)"></option>
|
||||
<option value="rgb(194, 133, 255)" label="rgb(194, 133, 255)"></option>
|
||||
<option value="rgb(136, 136, 136)" label="rgb(136, 136, 136)"></option>
|
||||
<option value="rgb(161, 0, 0)" label="rgb(161, 0, 0)"></option>
|
||||
<option value="rgb(178, 107, 0)" label="rgb(178, 107, 0)"></option>
|
||||
<option value="rgb(178, 178, 0)" label="rgb(178, 178, 0)"></option>
|
||||
<option value="rgb(0, 97, 0)" label="rgb(0, 97, 0)"></option>
|
||||
<option value="rgb(0, 71, 178)" label="rgb(0, 71, 178)"></option>
|
||||
<option value="rgb(107, 36, 178)" label="rgb(107, 36, 178)"></option>
|
||||
<option value="rgb(68, 68, 68)" label="rgb(68, 68, 68)"></option>
|
||||
<option value="rgb(92, 0, 0)" label="rgb(92, 0, 0)"></option>
|
||||
<option value="rgb(102, 61, 0)" label="rgb(102, 61, 0)"></option>
|
||||
<option value="rgb(102, 102, 0)" label="rgb(102, 102, 0)"></option>
|
||||
<option value="rgb(0, 55, 0)" label="rgb(0, 55, 0)"></option>
|
||||
<option value="rgb(0, 41, 102)" label="rgb(0, 41, 102)"></option>
|
||||
<option value="rgb(61, 20, 102)" label="rgb(61, 20, 102)"></option>
|
||||
</select>
|
||||
<span class="ql-format-separator"></span>
|
||||
<select title="Background Color" class="ql-background">
|
||||
<option value="rgb(0, 0, 0)" label="rgb(0, 0, 0)"></option>
|
||||
<option value="rgb(230, 0, 0)" label="rgb(230, 0, 0)"></option>
|
||||
<option value="rgb(255, 153, 0)" label="rgb(255, 153, 0)"></option>
|
||||
<option value="rgb(255, 255, 0)" label="rgb(255, 255, 0)"></option>
|
||||
<option value="rgb(0, 138, 0)" label="rgb(0, 138, 0)"></option>
|
||||
<option value="rgb(0, 102, 204)" label="rgb(0, 102, 204)"></option>
|
||||
<option value="rgb(153, 51, 255)" label="rgb(153, 51, 255)"></option>
|
||||
<option value="rgb(255, 255, 255)" label="rgb(255, 255, 255)" selected=""></option>
|
||||
<option value="rgb(250, 204, 204)" label="rgb(250, 204, 204)"></option>
|
||||
<option value="rgb(255, 235, 204)" label="rgb(255, 235, 204)"></option>
|
||||
<option value="rgb(255, 255, 204)" label="rgb(255, 255, 204)"></option>
|
||||
<option value="rgb(204, 232, 204)" label="rgb(204, 232, 204)"></option>
|
||||
<option value="rgb(204, 224, 245)" label="rgb(204, 224, 245)"></option>
|
||||
<option value="rgb(235, 214, 255)" label="rgb(235, 214, 255)"></option>
|
||||
<option value="rgb(187, 187, 187)" label="rgb(187, 187, 187)"></option>
|
||||
<option value="rgb(240, 102, 102)" label="rgb(240, 102, 102)"></option>
|
||||
<option value="rgb(255, 194, 102)" label="rgb(255, 194, 102)"></option>
|
||||
<option value="rgb(255, 255, 102)" label="rgb(255, 255, 102)"></option>
|
||||
<option value="rgb(102, 185, 102)" label="rgb(102, 185, 102)"></option>
|
||||
<option value="rgb(102, 163, 224)" label="rgb(102, 163, 224)"></option>
|
||||
<option value="rgb(194, 133, 255)" label="rgb(194, 133, 255)"></option>
|
||||
<option value="rgb(136, 136, 136)" label="rgb(136, 136, 136)"></option>
|
||||
<option value="rgb(161, 0, 0)" label="rgb(161, 0, 0)"></option>
|
||||
<option value="rgb(178, 107, 0)" label="rgb(178, 107, 0)"></option>
|
||||
<option value="rgb(178, 178, 0)" label="rgb(178, 178, 0)"></option>
|
||||
<option value="rgb(0, 97, 0)" label="rgb(0, 97, 0)"></option>
|
||||
<option value="rgb(0, 71, 178)" label="rgb(0, 71, 178)"></option>
|
||||
<option value="rgb(107, 36, 178)" label="rgb(107, 36, 178)"></option>
|
||||
<option value="rgb(68, 68, 68)" label="rgb(68, 68, 68)"></option>
|
||||
<option value="rgb(92, 0, 0)" label="rgb(92, 0, 0)"></option>
|
||||
<option value="rgb(102, 61, 0)" label="rgb(102, 61, 0)"></option>
|
||||
<option value="rgb(102, 102, 0)" label="rgb(102, 102, 0)"></option>
|
||||
<option value="rgb(0, 55, 0)" label="rgb(0, 55, 0)"></option>
|
||||
<option value="rgb(0, 41, 102)" label="rgb(0, 41, 102)"></option>
|
||||
<option value="rgb(61, 20, 102)" label="rgb(61, 20, 102)"></option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="ql-format-group">
|
||||
<span title="List" class="ql-format-button ql-list"></span>
|
||||
<span class="ql-format-separator"></span>
|
||||
<span title="Bullet" class="ql-format-button ql-bullet"></span>
|
||||
<span class="ql-format-separator"></span>
|
||||
<select title="Text Alignment" class="ql-align">
|
||||
<option value="left" label="Left" selected=""></option>
|
||||
<option value="center" label="Center"></option>
|
||||
<option value="right" label="Right"></option>
|
||||
<option value="justify" label="Justify"></option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="ql-format-group">
|
||||
<span title="Link" class="ql-format-button ql-link"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Create the editor container -->
|
||||
<div id="editor">
|
||||
<div id="quill-container">
|
||||
<div id="quill">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include the Quill library -->
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
|
||||
<script src="../bower_components/quill/dist/quill.js"></script>
|
||||
<script src="../bower_components/yjs/y.es6"></script>
|
||||
<script src="./index.js"></script>
|
||||
|
||||
@@ -7,8 +7,8 @@ Y({
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'richtext-example'
|
||||
name: 'webrtc',
|
||||
room: 'richtext-example-quill-beta'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
@@ -18,13 +18,22 @@ Y({
|
||||
window.yQuill = y
|
||||
|
||||
// create quill element
|
||||
window.quill = new Quill('#editor', {
|
||||
window.quill = new Quill('#quill', {
|
||||
modules: {
|
||||
'toolbar': { container: '#toolbar' },
|
||||
'link-tooltip': true
|
||||
formula: true,
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
[{ size: ['small', false, 'large', 'huge'] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{list: 'ordered' }]
|
||||
]
|
||||
},
|
||||
theme: 'snow'
|
||||
})
|
||||
});
|
||||
// bind quill to richtext type
|
||||
y.share.richtext.bind(window.quill)
|
||||
})
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
"y-websockets-client": "latest",
|
||||
"y-text": "latest",
|
||||
"y-indexeddb": "latest",
|
||||
"y-xml": "latest",
|
||||
"y-xml": "latest",
|
||||
"quill": "~0.20.1",
|
||||
"ace": "~1.2.3",
|
||||
"ace-builds": "~1.2.3",
|
||||
"jquery": "~2.2.2"
|
||||
"jquery": "~2.2.2",
|
||||
"d3": "^3.5.16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "11.0.1",
|
||||
"version": "11.2.6",
|
||||
"homepage": "y-js.org",
|
||||
"authors": [
|
||||
"Kevin Jahns <kevin.jahns@rwth-aachen.de>"
|
||||
|
||||
545
y.es6
545
y.es6
@@ -67,6 +67,16 @@ module.exports = function (Y/* :any */) {
|
||||
this.whenSyncedListeners = []
|
||||
return this.y.db.stopGarbageCollector()
|
||||
}
|
||||
repair () {
|
||||
console.info('Repairing the state of Yjs. This can happen if messages get lost, and Yjs detects that something is wrong. If this happens often, please report an issue here: https://github.com/y-js/yjs/issues')
|
||||
for (var name in this.connections) {
|
||||
this.connections[name].isSynced = false
|
||||
}
|
||||
this.isSynced = false
|
||||
this.currentSyncTarget = null
|
||||
this.broadcastedHB = false
|
||||
this.findNextSyncTarget()
|
||||
}
|
||||
setUserId (userId) {
|
||||
if (this.userId == null) {
|
||||
this.userId = userId
|
||||
@@ -421,7 +431,7 @@ module.exports = function (Y/* :any */) {
|
||||
module.exports = function (Y) {
|
||||
var globalRoom = {
|
||||
users: {},
|
||||
buffers: {}, // TODO: reimplement this idea. This does not cover all cases!! Here, you have a queue which is unrealistic (i.e. think about multiple incoming connections)
|
||||
buffers: {},
|
||||
removeUser: function (user) {
|
||||
for (var i in this.users) {
|
||||
this.users[i].userLeft(user)
|
||||
@@ -498,7 +508,7 @@ module.exports = function (Y) {
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
}, 10)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
globalRoom.whenTransactionsFinished().then(nextFlush)
|
||||
@@ -659,7 +669,7 @@ module.exports = function (Y /* :any */) {
|
||||
}
|
||||
this.gc1 = [] // first stage
|
||||
this.gc2 = [] // second stage -> after that, remove the op
|
||||
this.gcTimeout = !opts.gcTimeout ? 50000 : opts.gcTimeoutś
|
||||
this.gcTimeout = !opts.gcTimeout ? 50000 : opts.gcTimeouts
|
||||
function garbageCollect () {
|
||||
return os.whenTransactionsFinished().then(function () {
|
||||
if (os.gc1.length > 0 || os.gc2.length > 0) {
|
||||
@@ -696,6 +706,41 @@ module.exports = function (Y /* :any */) {
|
||||
if (this.gcTimeout > 0) {
|
||||
garbageCollect()
|
||||
}
|
||||
this.repairCheckInterval = !opts.repairCheckInterval ? 6000 : opts.repairCheckInterval
|
||||
this.opsReceivedTimestamp = new Date()
|
||||
this.startRepairCheck()
|
||||
}
|
||||
startRepairCheck () {
|
||||
var os = this
|
||||
if (this.repairCheckInterval > 0) {
|
||||
this.repairCheckIntervalHandler = setInterval(function repairOnMissingOperations () {
|
||||
/*
|
||||
Case 1. No ops have been received in a while (new Date() - os.opsReceivedTimestamp > os.repairCheckInterval)
|
||||
- 1.1 os.listenersById is empty. Then the state was correct the whole time. -> Nothing to do (nor to update)
|
||||
- 1.2 os.listenersById is not empty.
|
||||
* Then the state was incorrect for at least {os.repairCheckInterval} seconds.
|
||||
* -> Remove everything in os.listenersById and sync again (connector.repair())
|
||||
Case 2. An op has been received in the last {os.repairCheckInterval } seconds.
|
||||
It is not yet necessary to check for faulty behavior. Everything can still resolve itself. Wait for more messages.
|
||||
If nothing was received for a while and os.listenersById is still not emty, we are in case 1.2
|
||||
-> Do nothing
|
||||
|
||||
Baseline here is: we really only have to catch case 1.2..
|
||||
*/
|
||||
if (
|
||||
new Date() - os.opsReceivedTimestamp > os.repairCheckInterval &&
|
||||
Object.keys(os.listenersById).length > 0 // os.listenersById is not empty
|
||||
) {
|
||||
// haven't received operations for over {os.repairCheckInterval} seconds, resend state vector
|
||||
os.listenersById = {}
|
||||
os.opsReceivedTimestamp = new Date() // update so you don't send repair several times in a row
|
||||
os.y.connector.repair()
|
||||
}
|
||||
}, this.repairCheckInterval)
|
||||
}
|
||||
}
|
||||
stopRepairCheck () {
|
||||
clearInterval(this.repairCheckIntervalHandler)
|
||||
}
|
||||
queueGarbageCollector (id) {
|
||||
if (this.y.isConnected()) {
|
||||
@@ -791,6 +836,7 @@ module.exports = function (Y /* :any */) {
|
||||
* destroy () {
|
||||
clearInterval(this.gcInterval)
|
||||
this.gcInterval = null
|
||||
this.stopRepairCheck()
|
||||
for (var key in this.initializedTypes) {
|
||||
var type = this.initializedTypes[key]
|
||||
if (type._destroy != null) {
|
||||
@@ -830,12 +876,14 @@ module.exports = function (Y /* :any */) {
|
||||
/*
|
||||
Apply a list of operations.
|
||||
|
||||
* we save a timestamp, because we received new operations that could resolve ops in this.listenersById (see this.startRepairCheck)
|
||||
* get a transaction
|
||||
* check whether all Struct.*.requiredOps are in the OS
|
||||
* check if it is an expected op (otherwise wait for it)
|
||||
* check if was deleted, apply a delete operation after op was applied
|
||||
*/
|
||||
apply (ops) {
|
||||
this.opsReceivedTimestamp = new Date()
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
var o = ops[i]
|
||||
if (o.id == null || o.id[0] !== this.y.connector.userId) {
|
||||
@@ -951,68 +999,89 @@ module.exports = function (Y /* :any */) {
|
||||
}
|
||||
}
|
||||
if (defined == null) {
|
||||
var isGarbageCollected = yield* this.isGarbageCollected(op.id)
|
||||
var opid = op.id
|
||||
var isGarbageCollected = yield* this.isGarbageCollected(opid)
|
||||
if (!isGarbageCollected) {
|
||||
// TODO: reduce number of get / put calls for op ..
|
||||
yield* Y.Struct[op.struct].execute.call(this, op)
|
||||
yield* this.addOperation(op)
|
||||
yield* this.store.operationAdded(this, op)
|
||||
|
||||
// operationAdded can change op..
|
||||
op = yield* this.getOperation(opid)
|
||||
// if insertion, try to combine with left
|
||||
yield* this.tryCombineWithLeft(op)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// called by a transaction when an operation is added
|
||||
/*
|
||||
* Called by a transaction when an operation is added.
|
||||
* This function is especially important for y-indexeddb, where several instances may share a single database.
|
||||
* Every time an operation is created by one instance, it is send to all other instances and operationAdded is called
|
||||
*
|
||||
* If it's not a Delete operation:
|
||||
* * Checks if another operation is executable (listenersById)
|
||||
* * Update state, if possible
|
||||
*
|
||||
* Always:
|
||||
* * Call type
|
||||
*/
|
||||
* operationAdded (transaction, op) {
|
||||
// increase SS
|
||||
yield* transaction.updateState(op.id[0])
|
||||
|
||||
var opLen = op.content != null ? op.content.length : 1
|
||||
for (let i = 0; i < opLen; i++) {
|
||||
// notify whenOperation listeners (by id)
|
||||
var sid = JSON.stringify([op.id[0], op.id[1] + i])
|
||||
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.getInsertion(op.target)
|
||||
var type = this.initializedTypes[JSON.stringify(target.parent)]
|
||||
if (type != null) {
|
||||
yield* type._changed(transaction, op)
|
||||
}
|
||||
} else {
|
||||
// increase SS
|
||||
yield* transaction.updateState(op.id[0])
|
||||
var opLen = op.content != null ? op.content.length : 1
|
||||
for (let i = 0; i < opLen; i++) {
|
||||
// notify whenOperation listeners (by id)
|
||||
var sid = JSON.stringify([op.id[0], op.id[1] + i])
|
||||
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)]
|
||||
var t = this.initializedTypes[JSON.stringify(op.parent)]
|
||||
|
||||
// if parent is deleted, mark as gc'd and return
|
||||
if (op.parent != null) {
|
||||
var parentIsDeleted = yield* transaction.isDeleted(op.parent)
|
||||
if (parentIsDeleted) {
|
||||
yield* transaction.deleteList(op.id)
|
||||
return
|
||||
// if parent is deleted, mark as gc'd and return
|
||||
if (op.parent != null) {
|
||||
var parentIsDeleted = yield* transaction.isDeleted(op.parent)
|
||||
if (parentIsDeleted) {
|
||||
yield* transaction.deleteList(op.id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// notify parent, if it was instanciated as a custom type
|
||||
if (t != null) {
|
||||
let o = Y.utils.copyObject(op)
|
||||
yield* t._changed(transaction, o)
|
||||
}
|
||||
if (!op.deleted) {
|
||||
// Delete if DS says this is actually deleted
|
||||
var len = op.content != null ? op.content.length : 1
|
||||
var startId = op.id // You must not use op.id in the following loop, because op will change when deleted
|
||||
for (let i = 0; i < len; i++) {
|
||||
var id = [startId[0], startId[1] + i]
|
||||
var opIsDeleted = yield* transaction.isDeleted(id)
|
||||
if (opIsDeleted) {
|
||||
var delop = {
|
||||
struct: 'Delete',
|
||||
target: id
|
||||
// notify parent, if it was instanciated as a custom type
|
||||
if (t != null) {
|
||||
let o = Y.utils.copyOperation(op)
|
||||
yield* t._changed(transaction, o)
|
||||
}
|
||||
if (!op.deleted) {
|
||||
// Delete if DS says this is actually deleted
|
||||
var len = op.content != null ? op.content.length : 1
|
||||
var startId = op.id // You must not use op.id in the following loop, because op will change when deleted
|
||||
// TODO: !! console.log('TODO: change this before commiting')
|
||||
for (let i = 0; i < len; i++) {
|
||||
var id = [startId[0], startId[1] + i]
|
||||
var opIsDeleted = yield* transaction.isDeleted(id)
|
||||
if (opIsDeleted) {
|
||||
var delop = {
|
||||
struct: 'Delete',
|
||||
target: id
|
||||
}
|
||||
yield* this.tryExecute.call(transaction, delop)
|
||||
}
|
||||
yield* this.tryExecute.call(transaction, delop)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1063,14 +1132,9 @@ module.exports = function (Y /* :any */) {
|
||||
this.waitingTransactions.push(makeGen)
|
||||
if (!this.transactionInProgress) {
|
||||
this.transactionInProgress = true
|
||||
if (false || callImmediately) { // TODO: decide whether this is ok or not..
|
||||
setTimeout(() => {
|
||||
this.transact(this.getNextRequest())
|
||||
} else {
|
||||
var self = this
|
||||
setTimeout(function () {
|
||||
self.transact(self.getNextRequest())
|
||||
}, 0)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1746,14 +1810,11 @@ module.exports = function (Y/* :any */) {
|
||||
right = null
|
||||
}
|
||||
if (callType && !preventCallType) {
|
||||
var type = this.store.initializedTypes[JSON.stringify(target.parent)]
|
||||
if (type != null) {
|
||||
yield* type._changed(this, {
|
||||
struct: 'Delete',
|
||||
target: target.id,
|
||||
length: targetLength
|
||||
})
|
||||
}
|
||||
yield* this.store.operationAdded(this, {
|
||||
struct: 'Delete',
|
||||
target: target.id,
|
||||
length: targetLength
|
||||
})
|
||||
}
|
||||
// need to gc in the end!
|
||||
yield* this.store.addToGarbageCollector.call(this, target, left)
|
||||
@@ -1997,6 +2058,7 @@ module.exports = function (Y/* :any */) {
|
||||
if (o.right != null) {
|
||||
var right = yield* this.getOperation(o.right)
|
||||
right.left = o.left
|
||||
yield* this.setOperation(right)
|
||||
|
||||
if (o.originOf != null && o.originOf.length > 0) {
|
||||
// find new origin of right ops
|
||||
@@ -2063,10 +2125,6 @@ module.exports = function (Y/* :any */) {
|
||||
}
|
||||
// we don't need to set right here, because
|
||||
// right should be in o.originOf => it is set it the previous for loop
|
||||
} else {
|
||||
// we didn't need to reset the origin of right
|
||||
// so we have to set right here
|
||||
yield* this.setOperation(right)
|
||||
}
|
||||
}
|
||||
// o may originate in another operation.
|
||||
@@ -2495,6 +2553,7 @@ module.exports = function (Y/* :any */) {
|
||||
if (firstMissing != null) {
|
||||
// update startPos
|
||||
startPos = firstMissing.id[1]
|
||||
startSS[user] = startPos
|
||||
}
|
||||
}
|
||||
yield* this.os.iterate(this, [user, startPos], [user, Number.MAX_VALUE], function * (op) {
|
||||
@@ -2560,7 +2619,7 @@ module.exports = function (Y/* :any */) {
|
||||
/* this is what we used before.. use this as a reference..
|
||||
* makeOperationReady (startSS, op) {
|
||||
op = Y.Struct[op.struct].encode(op)
|
||||
op = Y.utils.copyObject(op)
|
||||
op = Y.utils.copyObject(op) -- use copyoperation instead now!
|
||||
var o = op
|
||||
var ids = [op.id]
|
||||
// search for the new op.right
|
||||
@@ -2686,6 +2745,153 @@ module.exports = function (Y /* : any*/) {
|
||||
receivedOp (op) {
|
||||
if (this.awaiting <= 0) {
|
||||
this.onevent(op)
|
||||
} else if (op.struct === 'Delete') {
|
||||
var self = this
|
||||
var checkDelete = function checkDelete (d) {
|
||||
if (d.length == null) {
|
||||
throw new Error('This shouldn\'t happen! d.length must be defined!')
|
||||
}
|
||||
// we check if o deletes something in self.waiting
|
||||
// if so, we remove the deleted operation
|
||||
for (var w = 0; w < self.waiting.length; w++) {
|
||||
var i = self.waiting[w]
|
||||
if (i.struct === 'Insert' && i.id[0] === d.target[0]) {
|
||||
var iLength = i.hasOwnProperty('content') ? i.content.length : 1
|
||||
var dStart = d.target[1]
|
||||
var dEnd = d.target[1] + (d.length || 1)
|
||||
var iStart = i.id[1]
|
||||
var iEnd = i.id[1] + iLength
|
||||
// Check if they don't overlap
|
||||
if (iEnd <= dStart || dEnd <= iStart) {
|
||||
// no overlapping
|
||||
continue
|
||||
}
|
||||
// we check all overlapping cases. All cases:
|
||||
/*
|
||||
1) iiiii
|
||||
ddddd
|
||||
--> modify i and d
|
||||
2) iiiiiii
|
||||
ddddd
|
||||
--> modify i, remove d
|
||||
3) iiiiiii
|
||||
ddd
|
||||
--> remove d, modify i, and create another i (for the right hand side)
|
||||
4) iiiii
|
||||
ddddddd
|
||||
--> remove i, modify d
|
||||
5) iiiiiii
|
||||
ddddddd
|
||||
--> remove both i and d (**)
|
||||
6) iiiiiii
|
||||
ddddd
|
||||
--> modify i, remove d
|
||||
7) iii
|
||||
ddddddd
|
||||
--> remove i, create and apply two d with checkDelete(d) (**)
|
||||
8) iiiii
|
||||
ddddddd
|
||||
--> remove i, modify d (**)
|
||||
9) iiiii
|
||||
ddddd
|
||||
--> modify i and d
|
||||
(**) (also check if i contains content or type)
|
||||
*/
|
||||
// TODO: I left some debugger statements, because I want to debug all cases once in production. REMEMBER END TODO
|
||||
if (iStart < dStart) {
|
||||
if (dStart < iEnd) {
|
||||
if (iEnd < dEnd) {
|
||||
// Case 1
|
||||
// remove the right part of i's content
|
||||
i.content.splice(dStart - iStart)
|
||||
// remove the start of d's deletion
|
||||
d.length = dEnd - iEnd
|
||||
d.target = [d.target[0], iEnd]
|
||||
continue
|
||||
} else if (iEnd === dEnd) {
|
||||
// Case 2
|
||||
i.content.splice(dStart - iStart)
|
||||
// remove d, we do that by simply ending this function
|
||||
return
|
||||
} else { // (dEnd < iEnd)
|
||||
// Case 3
|
||||
var newI = {
|
||||
id: [i.id[0], dEnd],
|
||||
content: i.content.slice(dEnd - iStart),
|
||||
struct: 'Insert'
|
||||
}
|
||||
self.waiting.push(newI)
|
||||
i.content.splice(dStart - iStart)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if (dStart === iStart) {
|
||||
if (iEnd < dEnd) {
|
||||
// Case 4
|
||||
d.length = dEnd - iEnd
|
||||
d.target = [d.target[0], iEnd]
|
||||
i.content = []
|
||||
continue
|
||||
} else if (iEnd === dEnd) {
|
||||
// Case 5
|
||||
self.waiting.splice(w, 1)
|
||||
return
|
||||
} else { // (dEnd < iEnd)
|
||||
// Case 6
|
||||
i.content = i.content.slice(dEnd - iStart)
|
||||
i.id = [i.id[0], dEnd]
|
||||
return
|
||||
}
|
||||
} else { // (dStart < iStart)
|
||||
if (iStart < dEnd) {
|
||||
// they overlap
|
||||
/*
|
||||
7) iii
|
||||
ddddddd
|
||||
--> remove i, create and apply two d with checkDelete(d) (**)
|
||||
8) iiiii
|
||||
ddddddd
|
||||
--> remove i, modify d (**)
|
||||
9) iiiii
|
||||
ddddd
|
||||
--> modify i and d
|
||||
*/
|
||||
if (iEnd < dEnd) {
|
||||
// Case 7
|
||||
// debugger // TODO: You did not test this case yet!!!! (add the debugger here)
|
||||
self.waiting.splice(w, 1)
|
||||
checkDelete({
|
||||
target: [d.target[0], dStart],
|
||||
length: iStart - dStart,
|
||||
struct: 'Delete'
|
||||
})
|
||||
checkDelete({
|
||||
target: [d.target[0], iEnd],
|
||||
length: iEnd - dEnd,
|
||||
struct: 'Delete'
|
||||
})
|
||||
return
|
||||
} else if (iEnd === dEnd) {
|
||||
// Case 8
|
||||
self.waiting.splice(w, 1)
|
||||
w--
|
||||
d.length -= iLength
|
||||
continue
|
||||
} else { // dEnd < iEnd
|
||||
// Case 9
|
||||
d.length = iStart - dStart
|
||||
i.content.splice(0, dEnd - iStart)
|
||||
i.id = [i.id[0], dEnd]
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// finished with remaining operations
|
||||
self.waiting.push(d)
|
||||
}
|
||||
checkDelete(op)
|
||||
} else {
|
||||
this.waiting.push(op)
|
||||
}
|
||||
@@ -2696,9 +2902,106 @@ module.exports = function (Y /* : any*/) {
|
||||
prematurely called operations are executed
|
||||
*/
|
||||
awaitAndPrematurelyCall (ops) {
|
||||
this.awaiting += ops.length
|
||||
ops.forEach(this.onevent)
|
||||
this.awaiting++
|
||||
ops.map(Y.utils.copyOperation).forEach(this.onevent)
|
||||
}
|
||||
* awaitOps (transaction, f, args) {
|
||||
function notSoSmartSort (array) {
|
||||
// this function sorts insertions in a executable order
|
||||
var result = []
|
||||
while (array.length > 0) {
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
var independent = true
|
||||
for (var j = 0; j < array.length; j++) {
|
||||
if (Y.utils.matchesId(array[j], array[i].left)) {
|
||||
// array[i] depends on array[j]
|
||||
independent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (independent) {
|
||||
result.push(array.splice(i, 1)[0])
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
var before = this.waiting.length
|
||||
// somehow create new operations
|
||||
yield* f.apply(transaction, args)
|
||||
// remove all appended ops / awaited ops
|
||||
this.waiting.splice(before)
|
||||
if (this.awaiting > 0) this.awaiting--
|
||||
// if there are no awaited ops anymore, we can update all waiting ops, and send execute them (if there are still no awaited ops)
|
||||
if (this.awaiting === 0 && this.waiting.length > 0) {
|
||||
// update all waiting ops
|
||||
for (let i = 0; i < this.waiting.length; i++) {
|
||||
var o = this.waiting[i]
|
||||
if (o.struct === 'Insert') {
|
||||
var _o = yield* transaction.getInsertion(o.id)
|
||||
if (!Y.utils.compareIds(_o.id, o.id)) {
|
||||
// o got extended
|
||||
o.left = [o.id[0], o.id[1] - 1]
|
||||
} else if (_o.left == null) {
|
||||
o.left = null
|
||||
} else {
|
||||
// find next undeleted op
|
||||
var left = yield* transaction.getInsertion(_o.left)
|
||||
while (left.deleted != null) {
|
||||
if (left.left != null) {
|
||||
left = yield* transaction.getInsertion(left.left)
|
||||
} else {
|
||||
left = null
|
||||
break
|
||||
}
|
||||
}
|
||||
o.left = left != null ? Y.utils.getLastId(left) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
// the previous stuff was async, so we have to check again!
|
||||
// We also pull changes from the bindings, if there exists such a method, this could increase awaiting too
|
||||
if (this._pullChanges != null) {
|
||||
this._pullChanges()
|
||||
}
|
||||
if (this.awaiting === 0) {
|
||||
// sort by type, execute inserts first
|
||||
var ins = []
|
||||
var dels = []
|
||||
this.waiting.forEach(function (o) {
|
||||
if (o.struct === 'Delete') {
|
||||
dels.push(o)
|
||||
} else {
|
||||
ins.push(o)
|
||||
}
|
||||
})
|
||||
this.waiting = []
|
||||
// put in executable order
|
||||
ins = notSoSmartSort(ins)
|
||||
// this.onevent can trigger the creation of another operation
|
||||
// -> check if this.awaiting increased & stop computation if it does
|
||||
for (var i = 0; i < ins.length; i++) {
|
||||
if (this.awaiting === 0) {
|
||||
this.onevent(ins[i])
|
||||
} else {
|
||||
this.waiting = this.waiting.concat(ins.slice(i))
|
||||
break
|
||||
}
|
||||
}
|
||||
for (i = 0; i < dels.length; i++) {
|
||||
if (this.awaiting === 0) {
|
||||
this.onevent(dels[i])
|
||||
} else {
|
||||
this.waiting = this.waiting.concat(dels.slice(i))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Remove awaitedInserts and awaitedDeletes in favor of awaitedOps, as they are deprecated and do not always work
|
||||
// Do this in one of the coming releases that are breaking anyway
|
||||
/*
|
||||
Call this when you successfully awaited the execution of n Insert operations
|
||||
*/
|
||||
@@ -2709,15 +3012,17 @@ module.exports = function (Y /* : any*/) {
|
||||
if (op.struct === 'Insert') {
|
||||
for (var i = this.waiting.length - 1; i >= 0; i--) {
|
||||
let w = this.waiting[i]
|
||||
// TODO: do I handle split operations correctly here? Super unlikely, but yeah..
|
||||
// Also: can this case happen? Can op be inserted in the middle of a larger op that is in $waiting?
|
||||
if (w.struct === 'Insert') {
|
||||
if (Y.utils.compareIds(op.left, w.id)) {
|
||||
if (Y.utils.matchesId(w, op.left)) {
|
||||
// include the effect of op in w
|
||||
w.right = op.id
|
||||
// exclude the effect of w in op
|
||||
op.left = w.left
|
||||
} else if (Y.utils.compareIds(op.right, w.id)) {
|
||||
} else if (Y.utils.compareIds(w.id, op.right)) {
|
||||
// similar..
|
||||
w.left = op.id
|
||||
w.left = Y.utils.getLastId(op)
|
||||
op.right = w.right
|
||||
}
|
||||
}
|
||||
@@ -2754,12 +3059,42 @@ module.exports = function (Y /* : any*/) {
|
||||
/* (private)
|
||||
Try to execute the events for the waiting operations
|
||||
*/
|
||||
_tryCallEvents (n) {
|
||||
this.awaiting -= n
|
||||
_tryCallEvents () {
|
||||
function notSoSmartSort (array) {
|
||||
var result = []
|
||||
while (array.length > 0) {
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
var independent = true
|
||||
for (var j = 0; j < array.length; j++) {
|
||||
if (Y.utils.matchesId(array[j], array[i].left)) {
|
||||
// array[i] depends on array[j]
|
||||
independent = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (independent) {
|
||||
result.push(array.splice(i, 1)[0])
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
if (this.awaiting > 0) this.awaiting--
|
||||
if (this.awaiting === 0 && this.waiting.length > 0) {
|
||||
var ops = this.waiting
|
||||
var ins = []
|
||||
var dels = []
|
||||
this.waiting.forEach(function (o) {
|
||||
if (o.struct === 'Delete') {
|
||||
dels.push(o)
|
||||
} else {
|
||||
ins.push(o)
|
||||
}
|
||||
})
|
||||
ins = notSoSmartSort(ins)
|
||||
ins.forEach(this.onevent)
|
||||
dels.forEach(this.onevent)
|
||||
this.waiting = []
|
||||
ops.forEach(this.onevent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2828,6 +3163,20 @@ module.exports = function (Y /* : any*/) {
|
||||
}
|
||||
Y.utils.copyObject = copyObject
|
||||
|
||||
/*
|
||||
Copy an operation, so that it can be manipulated.
|
||||
Note: You must not change subproperties (except o.content)!
|
||||
*/
|
||||
function copyOperation (o) {
|
||||
o = copyObject(o)
|
||||
if (o.content != null) {
|
||||
o.content = o.content.map(function (c) { return c })
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
Y.utils.copyOperation = copyOperation
|
||||
|
||||
/*
|
||||
Defines a smaller relation on Id's
|
||||
*/
|
||||
@@ -2836,6 +3185,11 @@ module.exports = function (Y /* : any*/) {
|
||||
}
|
||||
Y.utils.smaller = smaller
|
||||
|
||||
function inDeletionRange (del, ins) {
|
||||
return del.target[0] === ins[0] && del.target[1] <= ins[1] && ins[1] < del.target[1] + (del.length || 1)
|
||||
}
|
||||
Y.utils.inDeletionRange = inDeletionRange
|
||||
|
||||
function compareIds (id1, id2) {
|
||||
if (id1 == null || id2 == null) {
|
||||
return id1 === id2
|
||||
@@ -2892,9 +3246,9 @@ module.exports = function (Y /* : any*/) {
|
||||
I tried to optimize this for performance, therefore no highlevel operations.
|
||||
*/
|
||||
class SmallLookupBuffer extends Store {
|
||||
constructor (arg) {
|
||||
constructor (arg1, arg2) {
|
||||
// super(...arguments) -- do this when this is supported by stable nodejs
|
||||
super(arg)
|
||||
super(arg1, arg2)
|
||||
this.writeBuffer = createEmptyOpsArray(5)
|
||||
this.readBuffer = createEmptyOpsArray(10)
|
||||
}
|
||||
@@ -3087,6 +3441,7 @@ function requestModules (modules) {
|
||||
})
|
||||
promises.push(requireModule.promise)
|
||||
} else {
|
||||
console.info('YJS: Please do not depend on automatic requiring of modules anymore! Extend modules as follows `require(\'y-modulename\')(Y)`')
|
||||
require(modulename)(Y)
|
||||
}
|
||||
} else {
|
||||
@@ -3133,23 +3488,25 @@ function Y (opts/* :YOptions */) /* :Promise<YConfig> */ {
|
||||
modules.push(opts.share[name])
|
||||
}
|
||||
Y.sourceDir = opts.sourceDir
|
||||
return Y.requestModules(modules).then(function () {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (opts == null) reject('An options object is expected! ')
|
||||
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
|
||||
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
|
||||
else if (opts.db == null) reject('You must specify a database! (missing db property)')
|
||||
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
|
||||
else if (opts.share == null) reject('You must specify a set of shared types!')
|
||||
else {
|
||||
var yconfig = new YConfig(opts)
|
||||
yconfig.db.whenUserIdSet(function () {
|
||||
yconfig.init(function () {
|
||||
resolve(yconfig)
|
||||
return new Promise(function (resolve, reject) {
|
||||
setTimeout(function () {
|
||||
Y.requestModules(modules).then(function () {
|
||||
if (opts == null) reject('An options object is expected! ')
|
||||
else if (opts.connector == null) reject('You must specify a connector! (missing connector property)')
|
||||
else if (opts.connector.name == null) reject('You must specify connector name! (missing connector.name property)')
|
||||
else if (opts.db == null) reject('You must specify a database! (missing db property)')
|
||||
else if (opts.connector.name == null) reject('You must specify db name! (missing db.name property)')
|
||||
else if (opts.share == null) reject('You must specify a set of shared types!')
|
||||
else {
|
||||
var yconfig = new YConfig(opts)
|
||||
yconfig.db.whenUserIdSet(function () {
|
||||
yconfig.init(function () {
|
||||
resolve(yconfig)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}).catch(reject)
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user