@blockv/sdk
Version:
Allows web apps to display and interact with vatoms.
235 lines (207 loc) • 7.47 kB
JavaScript
import Region from '../Region'
import Vatom from '../../../model/Vatom'
import DataObjectAnimator from '../DataObjectAnimator'
import { merge } from 'lodash'
import Delayer from '../Delayer'
/**
* Intermediate class which handles updates via the BLOCKv websocket and returning Vatom objects. Regions can subclass this to automatically
* get updates via WebSocket.
*/
export default class BLOCKvRegion extends Region {
constructor (dataPool) {
super(dataPool)
// Queue of pending WebSocket messages
this.queuedMessages = []
this.socketPaused = false
this.socketProcessing = false
// Bind functions
this.onWebSocketMessage = this.onWebSocketMessage.bind(this)
// Add listeners for the WebSocket
this.socket = this.dataPool.Blockv.WebSockets
this.socket.connect()
this.socket.addEventListener('websocket.raw', this.onWebSocketMessage)
// Monitor for timed updates
DataObjectAnimator.addRegion(this)
}
/** Called when this region is going to be shut down */
close () {
super.close()
// Remove listeners
this.socket.removeEventListener('websocket.raw', this.onWebSocketMessage)
DataObjectAnimator.removeRegion(this)
}
/**
* Called to pause processing of websocket messages
* @private Called by subclasses.
*/
pauseMessages () {
this.socketPaused = true
}
/**
* Called to resume processing of websocket messages
*
* @private Called by subclasses.
*/
resumeMessages () {
// Unpause
this.socketPaused = false
// Process next message if needed
if (!this.socketProcessing) {
this.processNextMessage()
}
}
/**
* Called when there's a new event message via the WebSocket.
*
* @private
* @param {Object} msg The raw JSON from the websocket event message
*/
onWebSocketMessage (msg) {
// Add to queue
this.queuedMessages.push(msg)
// Process it if necessary
if (!this.socketPaused && !this.socketProcessing) {
this.processNextMessage()
}
}
/**
* Called to process the next WebSocket message.
*/
async processNextMessage () {
// Stop if socket is paused
if (this.socketPaused) {
return
}
// Stop if already processing
if (this.socketProcessing) {
return
}
this.socketProcessing = true
// Process message
try {
// Get next msg to process
let msg = this.queuedMessages.shift()
if (!msg) {
// No more messages!
this.socketProcessing = false
return
}
// Process message
await this.processMessage(msg)
} catch (err) {
// Error!
console.warn('[DataPool > BVWebSocketRegion] Error processing WebSocket message! ', err)
}
// Done, process next message
this.socketProcessing = false
this.processNextMessage()
}
/**
* Processes a WebSocket message.
*
* @private Called by BVWebSocketRegion.
* @abstract Subclasses can override to process other WebSocket messages. Always call super.processMessage(msg) though.
* @param {Object} msg The raw JSON from the websocket event message
*/
processMessage (msg) {
// Get vatom ID
let vatomID = msg.payload && msg.payload.id
if (!vatomID) {
return console.warn(`[DataPool > BVWebSocketRegion] Got websocket message with no vatom ID in it: `, msg)
}
// Ensure it's a state update message
if (msg.msg_type !== 'state_update' || !msg.payload.new_object) {
return
}
// Update existing objects
this.updateObjects([{
id: msg.payload.id,
new_data: msg.payload.new_object
}])
}
/** Map our data objects to Vatom objects */
map (object) {
// Only handle vatoms
if (object.type !== 'vatom') {
return null
}
// Fetch all faces linked to this vatom
let faces = Array.from(this.objects.values()).filter(o => o.type === 'face' && o.data.template === object.data['vAtom::vAtomType'].template).map(o => o.data)
// Fetch all actions linked to this vatom
let actions = Array.from(this.objects.values()).filter(o => o.type === 'action' && o.data.name.startsWith(object.data['vAtom::vAtomType'].template + '::Action::')).map(o => o.data)
// Create vatom object
return new Vatom(object.data, faces, actions)
}
/**
* Called when an object is about to be added.
*
* @private
* @abstract Can be overridden by subclasses which need to get these events.
* @param {DataObject} object The object which will be added.
*/
willAdd (object) {
// Notify parent as well
let parent = object.data && object.data['vAtom::vAtomType'] && object.data['vAtom::vAtomType'].parent_id
if (parent) {
Delayer.run(e => this.emit('object.updated', parent))
}
// If our DataObjectAnimator has a scheduled update for this object, include that change now. This is to work around map objects jumping around when a new region is created.
let nextUpdate = DataObjectAnimator.changes.find(u => u.id === object.id)
if (nextUpdate) {
merge(object.data, nextUpdate.new_data)
}
}
/**
* Called when an object is about to be updated.
*
* @private
* @abstract Can be overridden by subclasses which need to get these events.
* @param {DataObject} object The object which will be updated.
* @param {Object} newData The sparse object containing the changed fields
*/
willUpdateFields (object, newData) {
// Notify parent as well
let oldParent = object.data && object.data['vAtom::vAtomType'] && object.data['vAtom::vAtomType'].parent_id
let newParent = newData && newData['vAtom::vAtomType'] && newData['vAtom::vAtomType'].parent_id
if (newParent) { Delayer.run(e => this.emit('object.updated', oldParent)) }
if (newParent) { Delayer.run(e => this.emit('object.updated', newParent)) }
}
/**
* Called when an object is about to be updated.
*
* @private
* @abstract Can be overridden by subclasses which need to get these events.
* @param {DataObject} object The object which will be updated.
* @param {String} keyPath The field which will be changed.
* @param {*} oldValue The current field value.
* @param {*} newValue The new field value.
*/
willUpdateField (object, keyPath, oldValue, newValue) {
// Only do if modifying the parent ID field
if (keyPath !== 'vAtom::vAtomType.parent_id') {
return
}
// Notify parent
Delayer.run(e => this.emit('object.updated', oldValue))
Delayer.run(e => this.emit('object.updated', newValue))
}
/**
* Called when an object is about to be removed.
*
* @private
* @abstract Can be overridden by subclasses which need to get these events.
* @param {DataObject|String} objectOrID The object (or ID) which will be updated.
*/
willRemove (objectOrID) {
// Get object if needed
let object = objectOrID
if (typeof objectOrID === 'string') {
object = this.objects.get(objectOrID)
}
// Notify parent as well
let parent = object && object.data && object.data['vAtom::vAtomType'] && object.data['vAtom::vAtomType'].parent_id
if (parent) {
Delayer.run(e => this.emit('object.updated', parent))
}
}
}