yjs/provider/ydb/broadcastchannel.js
2018-11-25 03:17:00 +01:00

310 lines
9.1 KiB
JavaScript

/**
* @module provider/ydb
*/
/* eslint-env browser */
import * as decoding from '../../lib/decoding.js'
import * as encoding from '../../lib/encoding.js'
import * as globals from '../../lib/globals.js'
import * as NamedEventHandler from './NamedEventHandler.js'
const bc = new BroadcastChannel('ydb-client')
/**
* @type {Map<string, Set<Function>>}
*/
const datasubs = globals.createMap()
/**
* @type {Set<any>} Set of Ydb instances
*/
const ydbinstances = globals.createSet()
const bcRoomDataMessage = 0
const bcYdbCUConfCreated = 1
const bcYdbCUConfConfirmed = 2
const bcYdbRemoteOffsetReceived = 3
const bcYdbRemoteOffsetConfirmed = 4
const bcYdbSyncingRoomsToServer = 5
const bcYdbSyncFromServer = 6
export const getUnconfirmedRooms = ydb => {
const unconfirmedRooms = globals.createSet()
ydb.clientUnconfirmedStates.forEach(room => unconfirmedRooms.add(room))
return unconfirmedRooms
}
export const computeRoomState = (ydb, unconfirmedRooms, room) => {
// state is a RoomState, defined in YdbClient.js
const state = ydb.roomStates.get(room)
if (state === undefined) {
return {
upsynced: false,
downsynced: false,
persisted: false
}
}
return {
upsynced: !unconfirmedRooms.has(room),
downsynced: state.offset >= 0 && state.coffset >= state.offset,
persisted: state.coffset === state.offset && state.offset >= 0 && !unconfirmedRooms.has(room)
}
}
let roomStatesUpdating = []
const fireRoomStateUpdate = (ydb, room) => {
roomStatesUpdating.push(room)
if (roomStatesUpdating.length === 1) {
// first time this is called, trigger actual publisher
// setTimeout(() => {
const updated = new Map()
const unconfirmedRooms = getUnconfirmedRooms(ydb)
roomStatesUpdating.forEach(room => {
if (!updated.has(room)) {
updated.set(room, computeRoomState(ydb, unconfirmedRooms, room))
}
})
NamedEventHandler.fire(ydb, 'syncstate', {
updated
})
roomStatesUpdating = []
// }, 0)
}
}
const receiveBCData = data => {
const decoder = decoding.createDecoder(data)
while (decoding.hasContent(decoder)) {
const messageType = decoding.readVarUint(decoder)
switch (messageType) {
case bcRoomDataMessage: {
const room = decoding.readVarString(decoder)
const update = decoding.readTail(decoder)
const rsubs = datasubs.get(room)
if (rsubs !== undefined) {
rsubs.forEach(f => f(update))
}
break
}
case bcYdbCUConfCreated: {
const confid = decoding.readVarUint(decoder)
const room = decoding.readVarString(decoder)
ydbinstances.forEach(ydb => {
ydb.clientUnconfirmedStates.set(confid, room)
fireRoomStateUpdate(ydb, room)
})
break
}
case bcYdbCUConfConfirmed: {
const confid = decoding.readVarUint(decoder)
const offset = decoding.readVarUint(decoder)
ydbinstances.forEach(ydb => {
const room = ydb.clientUnconfirmedStates.get(confid)
if (room !== undefined) {
ydb.clientUnconfirmedStates.delete(confid)
const state = ydb.roomStates.get(room)
if (state.coffset < offset) {
state.coffset = offset
}
fireRoomStateUpdate(ydb, room)
}
})
break
}
case bcYdbRemoteOffsetReceived: {
const len = decoding.readVarUint(decoder)
for (let i = 0; i < len; i++) {
const room = decoding.readVarString(decoder)
const offset = decoding.readVarUint(decoder)
ydbinstances.forEach(ydb => {
// this is only called when an update is received
// so roomState.get(room) should exist
const state = ydb.roomStates.get(room)
if (state.coffset < offset) {
state.coffset = offset
}
fireRoomStateUpdate(ydb, room)
})
}
break
}
case bcYdbRemoteOffsetConfirmed: {
const len = decoding.readVarUint(decoder)
for (let i = 0; i < len; i++) {
const room = decoding.readVarString(decoder)
const offset = decoding.readVarUint(decoder)
ydbinstances.forEach(ydb => {
const state = ydb.roomStates.get(room)
state.offset = offset
fireRoomStateUpdate(ydb, room)
})
}
break
}
case bcYdbSyncingRoomsToServer: {
const len = decoding.readVarUint(decoder)
for (let i = 0; i < len; i++) {
const room = decoding.readVarString(decoder)
ydbinstances.forEach(ydb => {
const state = ydb.roomStates.get(room)
if (state === undefined) {
ydb.roomStates.set(room, {
rsid: -1,
offset: -1,
coffset: 0
})
fireRoomStateUpdate(ydb, room)
}
})
}
break
}
case bcYdbSyncFromServer: {
const len = decoding.readVarUint(decoder)
for (let i = 0; i < len; i++) {
const room = decoding.readVarString(decoder)
const offset = decoding.readVarUint(decoder)
const rsid = decoding.readVarUint(decoder)
ydbinstances.forEach(ydb => {
const state = ydb.roomStates.get(room)
state.offset = offset
state.rsid = rsid
fireRoomStateUpdate(ydb, room)
})
}
break
}
default:
globals.error('Unexpected bc message type')
}
}
}
bc.onmessage = event => receiveBCData(event.data)
/**
* Publish to all, including self
* @param {encoding.Encoder} encoder
*/
export const publishAll = encoder => {
const buffer = encoding.toBuffer(encoder)
bc.postMessage(buffer)
receiveBCData(buffer)
}
/**
* Call this when update was created by this user and confid was created
* @param {number} cconf
* @param {string} roomname
*/
export const _broadcastYdbCUConfCreated = (cconf, roomname) => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, bcYdbCUConfCreated)
encoding.writeVarUint(encoder, cconf)
encoding.writeVarString(encoder, roomname)
publishAll(encoder)
}
/**
* Call this when user confid was confirmed by host
* @param {number} cconf
* @param {number} offset The conf-offset of the client-created offset
*/
export const _broadcastYdbCUConfConfirmed = (cconf, offset) => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, bcYdbCUConfConfirmed)
encoding.writeVarUint(encoder, cconf)
encoding.writeVarUint(encoder, offset)
publishAll(encoder)
}
/**
* Call this when remote update is received (thus host has increased, but not confirmed, the offset)
* @param {Array<Object>} subs sub is { room, offset }
*/
export const _broadcastYdbRemoteOffsetReceived = subs => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, bcYdbRemoteOffsetReceived)
encoding.writeVarUint(encoder, subs.length)
subs.forEach(sub => {
encoding.writeVarString(encoder, sub.room)
encoding.writeVarUint(encoder, sub.offset)
})
publishAll(encoder)
}
/**
* @param {Array<Object>} subs sub is { room, offset }
*/
export const _broadcastYdbRemoteOffsetConfirmed = subs => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, bcYdbRemoteOffsetConfirmed)
encoding.writeVarUint(encoder, subs.length)
subs.forEach(sub => {
encoding.writeVarString(encoder, sub.room)
encoding.writeVarUint(encoder, sub.offset)
})
publishAll(encoder)
}
/**
* Call this when a subscription is created
* @param {Array<string>} rooms
*/
export const _broadcastYdbSyncingRoomsToServer = rooms => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, bcYdbSyncingRoomsToServer)
encoding.writeVarUint(encoder, rooms.length)
rooms.forEach(room => {
encoding.writeVarString(encoder, room)
})
publishAll(encoder)
}
/**
* Call this when sync confirmed by host
* @param {Array<Object>} subs sub is {room, offset, rsid}
*/
export const _broadcastYdbSyncFromServer = subs => {
const encoder = encoding.createEncoder()
encoding.writeVarUint(encoder, bcYdbSyncFromServer)
encoding.writeVarUint(encoder, subs.length)
subs.forEach(sub => {
encoding.writeVarString(encoder, sub.room)
encoding.writeVarUint(encoder, sub.offset)
encoding.writeVarUint(encoder, sub.rsid)
})
publishAll(encoder)
}
/**
* @param {string} room
* @param {Function} f
*/
export const subscribeRoomData = (room, f) => {
let rsubs = datasubs.get(room)
if (rsubs === undefined) {
rsubs = new Set()
datasubs.set(room, rsubs)
}
rsubs.add(f)
}
/**
* @param {string} room
* @param {ArrayBuffer} update
*/
export const publishRoomData = (room, update) => {
const encoder = encoding.createEncoder()
encoding.writeVarString(encoder, room)
encoding.writeArrayBuffer(encoder, update)
bc.postMessage(encoding.toBuffer(encoder))
// call subs directly here instead of calling receivedBCData
const rsubs = datasubs.get(room)
if (rsubs !== undefined) {
rsubs.forEach(f => f(update))
}
}
export const subscribeYdbEvents = ydb =>
ydbinstances.add(ydb)