/**
* yjs - A framework for real-time p2p shared editing on any data
* @version v13.0.0-12
* @license MIT
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Y = factory());
}(this, (function () { 'use strict';
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
/*! http://mths.be/fromcodepoint v0.2.1 by @mathias */
if (!String.fromCodePoint) {
(function() {
var defineProperty = (function() {
// IE 8 only supports `Object.defineProperty` on DOM elements
try {
var object = {};
var $defineProperty = Object.defineProperty;
var result = $defineProperty(object, object, object) && $defineProperty;
} catch(error) {}
return result;
}());
var stringFromCharCode = String.fromCharCode;
var floor = Math.floor;
var fromCodePoint = function(_) {
var MAX_SIZE = 0x4000;
var codeUnits = [];
var highSurrogate;
var lowSurrogate;
var index = -1;
var length = arguments.length;
if (!length) {
return '';
}
var result = '';
while (++index < length) {
var codePoint = Number(arguments[index]);
if (
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
codePoint < 0 || // not a valid Unicode code point
codePoint > 0x10FFFF || // not a valid Unicode code point
floor(codePoint) != codePoint // not an integer
) {
throw RangeError('Invalid code point: ' + codePoint);
}
if (codePoint <= 0xFFFF) { // BMP code point
codeUnits.push(codePoint);
} else { // Astral code point; split in surrogate halves
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
codePoint -= 0x10000;
highSurrogate = (codePoint >> 10) + 0xD800;
lowSurrogate = (codePoint % 0x400) + 0xDC00;
codeUnits.push(highSurrogate, lowSurrogate);
}
if (index + 1 == length || codeUnits.length > MAX_SIZE) {
result += stringFromCharCode.apply(null, codeUnits);
codeUnits.length = 0;
}
}
return result;
};
if (defineProperty) {
defineProperty(String, 'fromCodePoint', {
'value': fromCodePoint,
'configurable': true,
'writable': true
});
} else {
String.fromCodePoint = fromCodePoint;
}
}());
}
/*! http://mths.be/codepointat v0.2.0 by @mathias */
if (!String.prototype.codePointAt) {
(function() {
'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
var defineProperty = (function() {
// IE 8 only supports `Object.defineProperty` on DOM elements
try {
var object = {};
var $defineProperty = Object.defineProperty;
var result = $defineProperty(object, object, object) && $defineProperty;
} catch(error) {}
return result;
}());
var codePointAt = function(position) {
if (this == null) {
throw TypeError();
}
var string = String(this);
var size = string.length;
// `ToInteger`
var index = position ? Number(position) : 0;
if (index != index) { // better `isNaN`
index = 0;
}
// Account for out-of-bounds indices:
if (index < 0 || index >= size) {
return undefined;
}
// Get the first code unit
var first = string.charCodeAt(index);
var second;
if ( // check if it’s the start of a surrogate pair
first >= 0xD800 && first <= 0xDBFF && // high surrogate
size > index + 1 // there is a next code unit
) {
second = string.charCodeAt(index + 1);
if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
}
}
return first;
};
if (defineProperty) {
defineProperty(String.prototype, 'codePointAt', {
'value': codePointAt,
'configurable': true,
'writable': true
});
} else {
String.prototype.codePointAt = codePointAt;
}
}());
}
var UTF8_1 = createCommonjsModule(function (module) {
// UTF8 : Manage UTF-8 strings in ArrayBuffers
if(module.require) {
}
var UTF8={
// non UTF8 encoding detection (cf README file for details)
'isNotUTF8': function(bytes, byteOffset, byteLength) {
try {
UTF8.getStringFromBytes(bytes, byteOffset, byteLength, true);
} catch(e) {
return true;
}
return false;
},
// UTF8 decoding functions
'getCharLength': function(theByte) {
// 4 bytes encoded char (mask 11110000)
if(0xF0 == (theByte&0xF0)) {
return 4;
// 3 bytes encoded char (mask 11100000)
} else if(0xE0 == (theByte&0xE0)) {
return 3;
// 2 bytes encoded char (mask 11000000)
} else if(0xC0 == (theByte&0xC0)) {
return 2;
// 1 bytes encoded char
} else if(theByte == (theByte&0x7F)) {
return 1;
}
return 0;
},
'getCharCode': function(bytes, byteOffset, charLength) {
var charCode = 0, mask = '';
byteOffset = byteOffset || 0;
// Retrieve charLength if not given
charLength = charLength || UTF8.getCharLength(bytes[byteOffset]);
if(charLength == 0) {
throw new Error(bytes[byteOffset].toString(2)+' is not a significative' +
' byte (offset:'+byteOffset+').');
}
// Return byte value if charlength is 1
if(1 === charLength) {
return bytes[byteOffset];
}
// Test UTF8 integrity
mask = '00000000'.slice(0, charLength) + 1 + '00000000'.slice(charLength + 1);
if(bytes[byteOffset]&(parseInt(mask, 2))) {
throw Error('Index ' + byteOffset + ': A ' + charLength + ' bytes' +
' encoded char' +' cannot encode the '+(charLength+1)+'th rank bit to 1.');
}
// Reading the first byte
mask='0000'.slice(0,charLength+1)+'11111111'.slice(charLength+1);
charCode+=(bytes[byteOffset]&parseInt(mask,2))<<((--charLength)*6);
// Reading the next bytes
while(charLength) {
if(0x80!==(bytes[byteOffset+1]&0x80)
||0x40===(bytes[byteOffset+1]&0x40)) {
throw Error('Index '+(byteOffset+1)+': Next bytes of encoded char'
+' must begin with a "10" bit sequence.');
}
charCode += ((bytes[++byteOffset]&0x3F) << ((--charLength) * 6));
}
return charCode;
},
'getStringFromBytes': function(bytes, byteOffset, byteLength, strict) {
var charLength, chars = [];
byteOffset = byteOffset|0;
byteLength=('number' === typeof byteLength ?
byteLength :
bytes.byteLength || bytes.length
);
for(; byteOffset < byteLength; byteOffset++) {
charLength = UTF8.getCharLength(bytes[byteOffset]);
if(byteOffset + charLength > byteLength) {
if(strict) {
throw Error('Index ' + byteOffset + ': Found a ' + charLength +
' bytes encoded char declaration but only ' +
(byteLength - byteOffset) +' bytes are available.');
}
} else {
chars.push(String.fromCodePoint(
UTF8.getCharCode(bytes, byteOffset, charLength, strict)
));
}
byteOffset += charLength - 1;
}
return chars.join('');
},
// UTF8 encoding functions
'getBytesForCharCode': function(charCode) {
if(charCode < 128) {
return 1;
} else if(charCode < 2048) {
return 2;
} else if(charCode < 65536) {
return 3;
} else if(charCode < 2097152) {
return 4;
}
throw new Error('CharCode '+charCode+' cannot be encoded with UTF8.');
},
'setBytesFromCharCode': function(charCode, bytes, byteOffset, neededBytes) {
charCode = charCode|0;
bytes = bytes || [];
byteOffset = byteOffset|0;
neededBytes = neededBytes || UTF8.getBytesForCharCode(charCode);
// Setting the charCode as it to bytes if the byte length is 1
if(1 == neededBytes) {
bytes[byteOffset] = charCode;
} else {
// Computing the first byte
bytes[byteOffset++] =
(parseInt('1111'.slice(0, neededBytes), 2) << 8 - neededBytes) +
(charCode >>> ((--neededBytes) * 6));
// Computing next bytes
for(;neededBytes>0;) {
bytes[byteOffset++] = ((charCode>>>((--neededBytes) * 6))&0x3F)|0x80;
}
}
return bytes;
},
'setBytesFromString': function(string, bytes, byteOffset, byteLength, strict) {
string = string || '';
bytes = bytes || [];
byteOffset = byteOffset|0;
byteLength = ('number' === typeof byteLength ?
byteLength :
bytes.byteLength||Infinity
);
for(var i = 0, j = string.length; i < j; i++) {
var neededBytes = UTF8.getBytesForCharCode(string[i].codePointAt(0));
if(strict && byteOffset + neededBytes > byteLength) {
throw new Error('Not enought bytes to encode the char "' + string[i] +
'" at the offset "' + byteOffset + '".');
}
UTF8.setBytesFromCharCode(string[i].codePointAt(0),
bytes, byteOffset, neededBytes, strict);
byteOffset += neededBytes;
}
return bytes;
}
};
{
module.exports = UTF8;
}
});
const bits7 = 0b1111111;
const bits8 = 0b11111111;
class BinaryEncoder {
constructor () {
this.data = [];
}
get pos () {
return this.data.length
}
createBuffer () {
return Uint8Array.from(this.data).buffer
}
writeUint8 (num) {
this.data.push(num & bits8);
}
setUint8 (pos, num) {
this.data[pos] = num & bits8;
}
writeUint16 (num) {
this.data.push(num & bits8, (num >>> 8) & bits8);
}
setUint16 (pos, num) {
this.data[pos] = num & bits8;
this.data[pos + 1] = (num >>> 8) & bits8;
}
writeUint32 (num) {
for (let i = 0; i < 4; i++) {
this.data.push(num & bits8);
num >>>= 8;
}
}
setUint32 (pos, num) {
for (let i = 0; i < 4; i++) {
this.data[pos + i] = num & bits8;
num >>>= 8;
}
}
writeVarUint (num) {
while (num >= 0b10000000) {
this.data.push(0b10000000 | (bits7 & num));
num >>>= 7;
}
this.data.push(bits7 & num);
}
writeVarString (str) {
let bytes = UTF8_1.setBytesFromString(str);
let len = bytes.length;
this.writeVarUint(len);
for (let i = 0; i < len; i++) {
this.data.push(bytes[i]);
}
}
writeOpID (id) {
let user = id[0];
this.writeVarUint(user);
if (user !== 0xFFFFFF) {
this.writeVarUint(id[1]);
} else {
this.writeVarString(id[1]);
}
}
}
class BinaryDecoder {
constructor (buffer) {
if (buffer instanceof ArrayBuffer) {
this.uint8arr = new Uint8Array(buffer);
} else if (buffer instanceof Uint8Array) {
this.uint8arr = buffer;
} else {
throw new Error('Expected an ArrayBuffer or Uint8Array!')
}
this.pos = 0;
}
skip8 () {
this.pos++;
}
readUint8 () {
return this.uint8arr[this.pos++]
}
readUint32 () {
let uint =
this.uint8arr[this.pos] +
(this.uint8arr[this.pos + 1] << 8) +
(this.uint8arr[this.pos + 2] << 16) +
(this.uint8arr[this.pos + 3] << 24);
this.pos += 4;
return uint
}
peekUint8 () {
return this.uint8arr[this.pos]
}
readVarUint () {
let num = 0;
let len = 0;
while (true) {
let r = this.uint8arr[this.pos++];
num = num | ((r & bits7) << len);
len += 7;
if (r < 1 << 7) {
return num >>> 0 // return unsigned number!
}
if (len > 35) {
throw new Error('Integer out of range!')
}
}
}
readVarString () {
let len = this.readVarUint();
let bytes = new Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = this.uint8arr[this.pos++];
}
return UTF8_1.getStringFromBytes(bytes)
}
peekVarString () {
let pos = this.pos;
let s = this.readVarString();
this.pos = pos;
return s
}
readOpID () {
let user = this.readVarUint();
if (user !== 0xFFFFFF) {
return [user, this.readVarUint()]
} else {
return [user, this.readVarString()]
}
}
}
function formatYjsMessage (buffer) {
let decoder = new BinaryDecoder(buffer);
decoder.readVarString(); // read roomname
let type = decoder.readVarString();
let strBuilder = [];
strBuilder.push('\n === ' + type + ' ===\n');
if (type === 'update') {
logMessageUpdate(decoder, strBuilder);
} else if (type === 'sync step 1') {
logMessageSyncStep1(decoder, strBuilder);
} else if (type === 'sync step 2') {
logMessageSyncStep2(decoder, strBuilder);
} else {
strBuilder.push('-- Unknown message type - probably an encoding issue!!!');
}
return strBuilder.join('')
}
function formatYjsMessageType (buffer) {
let decoder = new BinaryDecoder(buffer);
decoder.readVarString(); // roomname
return decoder.readVarString()
}
function logMessageUpdate (decoder, strBuilder) {
let len = decoder.readUint32();
for (let i = 0; i < len; i++) {
strBuilder.push(JSON.stringify(Y.Struct.binaryDecodeOperation(decoder)) + '\n');
}
}
function computeMessageUpdate (decoder, encoder, conn) {
if (conn.y.db.forwardAppliedOperations) {
let messagePosition = decoder.pos;
let len = decoder.readUint32();
let delops = [];
for (let i = 0; i < len; i++) {
let op = Y.Struct.binaryDecodeOperation(decoder);
if (op.struct === 'Delete') {
delops.push(op);
}
}
if (delops.length > 0) {
conn.broadcastOps(delops);
}
decoder.pos = messagePosition;
}
conn.y.db.applyOperations(decoder);
}
function sendSyncStep1 (conn, syncUser) {
conn.y.db.requestTransaction(function * () {
let encoder = new BinaryEncoder();
encoder.writeVarString(conn.opts.room || '');
encoder.writeVarString('sync step 1');
encoder.writeVarString(conn.authInfo || '');
encoder.writeVarUint(conn.protocolVersion);
let preferUntransformed = conn.preferUntransformed && this.os.length === 0; // TODO: length may not be defined
encoder.writeUint8(preferUntransformed ? 1 : 0);
yield * this.writeStateSet(encoder);
conn.send(syncUser, encoder.createBuffer());
});
}
function logMessageSyncStep1 (decoder, strBuilder) {
let auth = decoder.readVarString();
let protocolVersion = decoder.readVarUint();
let preferUntransformed = decoder.readUint8() === 1;
strBuilder.push(`
- auth: "${auth}"
- protocolVersion: ${protocolVersion}
- preferUntransformed: ${preferUntransformed}
`);
logSS(decoder, strBuilder);
}
function computeMessageSyncStep1 (decoder, encoder, conn, senderConn, sender) {
let protocolVersion = decoder.readVarUint();
let preferUntransformed = decoder.readUint8() === 1;
// check protocol version
if (protocolVersion !== conn.protocolVersion) {
console.warn(
`You tried to sync with a yjs instance that has a different protocol version
(You: ${protocolVersion}, Client: ${protocolVersion}).
The sync was stopped. You need to upgrade your dependencies (especially Yjs & the Connector)!
`);
conn.y.destroy();
}
return conn.y.db.whenTransactionsFinished().then(() => {
// send sync step 2
conn.y.db.requestTransaction(function * () {
encoder.writeVarString('sync step 2');
encoder.writeVarString(conn.authInfo || '');
if (preferUntransformed) {
encoder.writeUint8(1);
yield * this.writeOperationsUntransformed(encoder);
} else {
encoder.writeUint8(0);
yield * this.writeOperations(encoder, decoder);
}
yield * this.writeDeleteSet(encoder);
conn.send(senderConn.uid, encoder.createBuffer());
senderConn.receivedSyncStep2 = true;
});
return conn.y.db.whenTransactionsFinished().then(() => {
if (conn.role === 'slave') {
sendSyncStep1(conn, sender);
}
})
})
}
function logSS (decoder, strBuilder) {
strBuilder.push(' == SS: \n');
let len = decoder.readUint32();
for (let i = 0; i < len; i++) {
let user = decoder.readVarUint();
let clock = decoder.readVarUint();
strBuilder.push(` ${user}: ${clock}\n`);
}
}
function logOS (decoder, strBuilder) {
strBuilder.push(' == OS: \n');
let len = decoder.readUint32();
for (let i = 0; i < len; i++) {
let op = Y.Struct.binaryDecodeOperation(decoder);
strBuilder.push(JSON.stringify(op) + '\n');
}
}
function logDS (decoder, strBuilder) {
strBuilder.push(' == DS: \n');
let len = decoder.readUint32();
for (let i = 0; i < len; i++) {
let user = decoder.readVarUint();
strBuilder.push(` User: ${user}: `);
let len2 = decoder.readVarUint();
for (let j = 0; j < len2; j++) {
let from = decoder.readVarUint();
let to = decoder.readVarUint();
let gc = decoder.readUint8() === 1;
strBuilder.push(`[${from}, ${to}, ${gc}]`);
}
}
}
function logMessageSyncStep2 (decoder, strBuilder) {
strBuilder.push(' - auth: ' + decoder.readVarString() + '\n');
let osTransformed = decoder.readUint8() === 1;
strBuilder.push(' - osUntransformed: ' + osTransformed + '\n');
logOS(decoder, strBuilder);
if (osTransformed) {
logSS(decoder, strBuilder);
}
logDS(decoder, strBuilder);
}
function computeMessageSyncStep2 (decoder, encoder, conn, senderConn, sender) {
var db = conn.y.db;
let defer = senderConn.syncStep2;
// apply operations first
db.requestTransaction(function * () {
let osUntransformed = decoder.readUint8();
if (osUntransformed === 1) {
yield * this.applyOperationsUntransformed(decoder);
} else {
this.store.applyOperations(decoder);
}
});
// then apply ds
db.requestTransaction(function * () {
yield * this.applyDeleteSet(decoder);
});
return db.whenTransactionsFinished().then(() => {
conn._setSyncedWith(sender);
defer.resolve();
})
}
function extendConnector (Y/* :any */) {
class AbstractConnector {
/*
opts contains the following information:
role : String Role of this client ("master" or "slave")
*/
constructor (y, opts) {
this.y = y;
if (opts == null) {
opts = {};
}
this.opts = opts;
// Prefer to receive untransformed operations. This does only work if
// this client receives operations from only one other client.
// In particular, this does not work with y-webrtc.
// It will work with y-websockets-client
this.preferUntransformed = opts.preferUntransformed || false;
if (opts.role == null || opts.role === 'master') {
this.role = 'master';
} else if (opts.role === 'slave') {
this.role = 'slave';
} else {
throw new Error("Role must be either 'master' or 'slave'!")
}
this.log = Y.debug('y:connector');
this.logMessage = Y.debug('y:connector-message');
this.y.db.forwardAppliedOperations = opts.forwardAppliedOperations || false;
this.role = opts.role;
this.connections = new Map();
this.isSynced = false;
this.userEventListeners = [];
this.whenSyncedListeners = [];
this.currentSyncTarget = null;
this.debug = opts.debug === true;
this.broadcastOpBuffer = [];
this.protocolVersion = 11;
this.authInfo = opts.auth || null;
this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') }; // default is everyone has write access
if (opts.generateUserId !== false) {
this.setUserId(Y.utils.generateUserId());
}
}
reconnect () {
this.log('reconnecting..');
return this.y.db.startGarbageCollector()
}
disconnect () {
this.log('discronnecting..');
this.connections = new Map();
this.isSynced = false;
this.currentSyncTarget = null;
this.whenSyncedListeners = [];
this.y.db.stopGarbageCollector();
return this.y.db.whenTransactionsFinished()
}
repair () {
this.log('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');
this.isSynced = false;
this.connections.forEach((user, userId) => {
user.isSynced = false;
this._syncWithUser(userId);
});
}
setUserId (userId) {
if (this.userId == null) {
if (!Number.isInteger(userId)) {
let err = new Error('UserId must be an integer!');
this.y.emit('error', err);
throw err
}
this.log('Set userId to "%s"', userId);
this.userId = userId;
return this.y.db.setUserId(userId)
} else {
return null
}
}
onUserEvent (f) {
this.userEventListeners.push(f);
}
removeUserEventListener (f) {
this.userEventListeners = this.userEventListeners.filter(g => f !== g);
}
userLeft (user) {
if (this.connections.has(user)) {
this.log('%s: User left %s', this.userId, user);
this.connections.delete(user);
// check if isSynced event can be sent now
this._setSyncedWith(null);
for (var f of this.userEventListeners) {
f({
action: 'userLeft',
user: user
});
}
}
}
userJoined (user, role, auth) {
if (role == null) {
throw new Error('You must specify the role of the joined user!')
}
if (this.connections.has(user)) {
throw new Error('This user already joined!')
}
this.log('%s: User joined %s', this.userId, user);
this.connections.set(user, {
uid: user,
isSynced: false,
role: role,
processAfterAuth: [],
auth: auth || null,
receivedSyncStep2: false
});
let defer = {};
defer.promise = new Promise(function (resolve) { defer.resolve = resolve; });
this.connections.get(user).syncStep2 = defer;
for (var f of this.userEventListeners) {
f({
action: 'userJoined',
user: user,
role: role
});
}
this._syncWithUser(user);
}
// Execute a function _when_ we are connected.
// If not connected, wait until connected
whenSynced (f) {
if (this.isSynced) {
f();
} else {
this.whenSyncedListeners.push(f);
}
}
_syncWithUser (userid) {
if (this.role === 'slave') {
return // "The current sync has not finished or this is controlled by a master!"
}
sendSyncStep1(this, userid);
}
_fireIsSyncedListeners () {
this.y.db.whenTransactionsFinished().then(() => {
if (!this.isSynced) {
this.isSynced = true;
// It is safer to remove this!
// TODO: remove: yield * this.garbageCollectAfterSync()
// call whensynced listeners
for (var f of this.whenSyncedListeners) {
f();
}
this.whenSyncedListeners = [];
}
});
}
send (uid, buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
}
this.log('%s: Send \'%y\' to %s', this.userId, buffer, uid);
this.logMessage('Message: %Y', buffer);
}
broadcast (buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - please don\'t use this method to send custom messages')
}
this.log('%s: Broadcast \'%y\'', this.userId, buffer);
this.logMessage('Message: %Y', buffer);
}
/*
Buffer operations, and broadcast them when ready.
*/
broadcastOps (ops) {
ops = ops.map(function (op) {
return Y.Struct[op.struct].encode(op)
});
var self = this;
function broadcastOperations () {
if (self.broadcastOpBuffer.length > 0) {
let encoder = new BinaryEncoder();
encoder.writeVarString(self.opts.room);
encoder.writeVarString('update');
let ops = self.broadcastOpBuffer;
self.broadcastOpBuffer = [];
let length = ops.length;
encoder.writeUint32(length);
for (var i = 0; i < length; i++) {
let op = ops[i];
Y.Struct[op.struct].binaryEncode(encoder, op);
}
self.broadcast(encoder.createBuffer());
}
}
if (this.broadcastOpBuffer.length === 0) {
this.broadcastOpBuffer = ops;
this.y.db.whenTransactionsFinished().then(broadcastOperations);
} else {
this.broadcastOpBuffer = this.broadcastOpBuffer.concat(ops);
}
}
/*
You received a raw message, and you know that it is intended for Yjs. Then call this function.
*/
receiveMessage (sender, buffer) {
if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
}
if (sender === this.userId) {
return Promise.resolve()
}
let decoder = new BinaryDecoder(buffer);
let encoder = new BinaryEncoder();
let roomname = decoder.readVarString(); // read room name
encoder.writeVarString(roomname);
let messageType = decoder.readVarString();
let senderConn = this.connections.get(sender);
this.log('%s: Receive \'%s\' from %s', this.userId, messageType, sender);
this.logMessage('Message: %Y', buffer);
if (senderConn == null) {
throw new Error('Received message from unknown peer!')
}
if (messageType === 'sync step 1' || messageType === 'sync step 2') {
let auth = decoder.readVarUint();
if (senderConn.auth == null) {
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender]);
// check auth
return this.checkAuth(auth, this.y, sender).then(authPermissions => {
if (senderConn.auth == null) {
senderConn.auth = authPermissions;
this.y.emit('userAuthenticated', {
user: senderConn.uid,
auth: authPermissions
});
}
let messages = senderConn.processAfterAuth;
senderConn.processAfterAuth = [];
return messages.reduce((p, m) =>
p.then(() => this.computeMessage(m[0], m[1], m[2], m[3], m[4]))
, Promise.resolve())
})
}
}
if (senderConn.auth != null) {
return this.computeMessage(messageType, senderConn, decoder, encoder, sender)
} else {
senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender]);
}
}
computeMessage (messageType, senderConn, decoder, encoder, sender) {
if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
// cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
computeMessageSyncStep1(decoder, encoder, this, senderConn, sender);
return this.y.db.whenTransactionsFinished()
} else if (messageType === 'sync step 2' && senderConn.auth === 'write') {
return computeMessageSyncStep2(decoder, encoder, this, senderConn, sender)
} else if (messageType === 'update' && senderConn.auth === 'write') {
return computeMessageUpdate(decoder, encoder, this, senderConn, sender)
} else {
return Promise.reject(new Error('Unable to receive message'))
}
}
_setSyncedWith (user) {
if (user != null) {
this.connections.get(user).isSynced = true;
}
let conns = Array.from(this.connections.values());
if (conns.length > 0 && conns.every(u => u.isSynced)) {
this._fireIsSyncedListeners();
}
}
/*
Currently, the HB encodes operations as JSON. For the moment I want to keep it
that way. Maybe we support encoding in the HB as XML in the future, but for now I don't want
too much overhead. Y is very likely to get changed a lot in the future
Because we don't want to encode JSON as string (with character escaping, wich makes it pretty much unreadable)
we encode the JSON as XML.
When the HB support encoding as XML, the format should look pretty much like this.
does not support primitive values as array elements
expects an ltx (less than xml) object
*/
parseMessageFromXml (m/* :any */) {
function parseArray (node) {
for (var n of node.children) {
if (n.getAttribute('isArray') === 'true') {
return parseArray(n)
} else {
return parseObject(n)
}
}
}
function parseObject (node/* :any */) {
var json = {};
for (var attrName in node.attrs) {
var value = node.attrs[attrName];
var int = parseInt(value, 10);
if (isNaN(int) || ('' + int) !== value) {
json[attrName] = value;
} else {
json[attrName] = int;
}
}
for (var n/* :any */ in node.children) {
var name = n.name;
if (n.getAttribute('isArray') === 'true') {
json[name] = parseArray(n);
} else {
json[name] = parseObject(n);
}
}
return json
}
parseObject(m);
}
/*
encode message in xml
we use string because Strophe only accepts an "xml-string"..
So {a:4,b:{c:5}} will look like
m - ltx element
json - Object
*/
encodeMessageToXml (msg, obj) {
// attributes is optional
function encodeObject (m, json) {
for (var name in json) {
var value = json[name];
if (name == null) {
// nop
} else if (value.constructor === Object) {
encodeObject(m.c(name), value);
} else if (value.constructor === Array) {
encodeArray(m.c(name), value);
} else {
m.setAttribute(name, value);
}
}
}
function encodeArray (m, array) {
m.setAttribute('isArray', 'true');
for (var e of array) {
if (e.constructor === Object) {
encodeObject(m.c('array-element'), e);
} else {
encodeArray(m.c('array-element'), e);
}
}
}
if (obj.constructor === Object) {
encodeObject(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj);
} else if (obj.constructor === Array) {
encodeArray(msg.c('y', { xmlns: 'http://y.ninja/connector-stanza' }), obj);
} else {
throw new Error("I can't encode this json!")
}
}
}
Y.AbstractConnector = AbstractConnector;
}
/* @flow */
function extendDatabase (Y /* :any */) {
/*
Partial definition of an OperationStore.
TODO: name it Database, operation store only holds operations.
A database definition must alse define the following methods:
* logTable() (optional)
- show relevant information information in a table
* requestTransaction(makeGen)
- request a transaction
* destroy()
- destroy the database
*/
class AbstractDatabase {
/* ::
y: YConfig;
forwardAppliedOperations: boolean;
listenersById: Object;
listenersByIdExecuteNow: Array