/** * @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>} */ const datasubs = globals.createMap() /** * Set of Ydb instances * @type {Set} */ 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} 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} 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} 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} 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)