mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-05-01 23:59:50 +00:00
Resolve "Real time collaboration"
This commit is contained in:
parent
b37886dea2
commit
83c4912fc7
100 changed files with 3288 additions and 156 deletions
web-frontend/modules/core/plugins
204
web-frontend/modules/core/plugins/realTimeHandler.js
Normal file
204
web-frontend/modules/core/plugins/realTimeHandler.js
Normal file
|
@ -0,0 +1,204 @@
|
|||
import { isSecureURL } from '@baserow/modules/core/utils/string'
|
||||
|
||||
export class RealTimeHandler {
|
||||
constructor(context) {
|
||||
this.context = context
|
||||
this.socket = null
|
||||
this.connected = false
|
||||
this.reconnect = false
|
||||
this.reconnectTimeout = null
|
||||
this.events = {}
|
||||
this.attempts = 0
|
||||
this.page = null
|
||||
this.pageParameters = {}
|
||||
this.subscribedToPage = true
|
||||
this.registerCoreEvents()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new connection with to the web socket so that real time updates can be
|
||||
* received.
|
||||
*/
|
||||
connect(reconnect = true) {
|
||||
this.reconnect = reconnect
|
||||
|
||||
const token = this.context.store.getters['auth/token']
|
||||
|
||||
// The web socket url is the same as the PUBLIC_BACKEND_URL apart from the
|
||||
// protocol.
|
||||
const rawUrl = this.context.app.$env.PUBLIC_BACKEND_URL
|
||||
const url = new URL(rawUrl)
|
||||
url.protocol = isSecureURL(rawUrl) ? 'wss:' : 'ws:'
|
||||
url.pathname = '/ws/core/'
|
||||
|
||||
this.socket = new WebSocket(`${url}?jwt_token=${token}`)
|
||||
this.socket.onopen = () => {
|
||||
this.context.store.dispatch('notification/setConnecting', false)
|
||||
this.connected = true
|
||||
this.attempts = 0
|
||||
|
||||
// If the client needs to be subscribed to a page we can do that directly
|
||||
// after connecting.
|
||||
if (!this.subscribedToPage) {
|
||||
this.subscribeToPage()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The received messages are always JSON so we need to the parse it, extract the
|
||||
* type and call the correct event.
|
||||
*/
|
||||
this.socket.onmessage = (message) => {
|
||||
let data = {}
|
||||
|
||||
try {
|
||||
data = JSON.parse(message.data)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(data, 'type') &&
|
||||
Object.prototype.hasOwnProperty.call(this.events, data.type)
|
||||
) {
|
||||
this.events[data.type](this.context, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the connection closes we want to reconnect immediately because we don't
|
||||
* want to miss any important real time updates. After the first attempt we want to
|
||||
* delay retry with 5 seconds.
|
||||
*/
|
||||
this.socket.onclose = () => {
|
||||
this.connected = false
|
||||
// By default the user not subscribed to a page a.k.a `null`, so if the current
|
||||
// page is already null we can mark it as subscribed.
|
||||
this.subscribedToPage = this.page === null
|
||||
|
||||
// Automatically reconnect if the socket closes.
|
||||
if (this.reconnect) {
|
||||
this.attempts++
|
||||
this.context.store.dispatch('notification/setConnecting', true)
|
||||
|
||||
this.reconnectTimeout = setTimeout(
|
||||
() => {
|
||||
this.connect(true)
|
||||
},
|
||||
// After the first try, we want to try again every 5 seconds.
|
||||
this.attempts > 0 ? 5000 : 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes the client to a given page. After subscribing the client will
|
||||
* receive updated related to that page. This is for example used when a user
|
||||
* opens a table page.
|
||||
*/
|
||||
subscribe(page, parameters) {
|
||||
this.page = page
|
||||
this.pageParameters = parameters
|
||||
this.subscribedToPage = false
|
||||
|
||||
// If the client is already connected we can directly subscribe to the page.
|
||||
if (this.connected) {
|
||||
this.subscribeToPage()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the real time server that updates for a certain page +
|
||||
* parameters must be received.
|
||||
*/
|
||||
subscribeToPage() {
|
||||
this.socket.send(
|
||||
JSON.stringify({
|
||||
page: this.page === null ? '' : this.page,
|
||||
...this.pageParameters,
|
||||
})
|
||||
)
|
||||
this.subscribedToPage = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the socket and resets all the variables. The can be used when
|
||||
* navigating to another page that doesn't require updates.
|
||||
*/
|
||||
disconnect() {
|
||||
if (!this.connected) {
|
||||
return
|
||||
}
|
||||
|
||||
this.context.store.dispatch('notification/setConnecting', false)
|
||||
clearTimeout(this.reconnectTimeout)
|
||||
this.reconnect = false
|
||||
this.attempts = 0
|
||||
this.connected = false
|
||||
this.socket.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new event with the event registry.
|
||||
*/
|
||||
registerEvent(type, callback) {
|
||||
this.events[type] = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all the core event handlers, which is for the groups and applications.
|
||||
*/
|
||||
registerCoreEvents() {
|
||||
// When the authentication is successful we want to store the web socket id in
|
||||
// auth store. Every AJAX request will include the web socket id as header, this
|
||||
// way the backend knows that this client does not has to receive the event
|
||||
// because we already know about the change.
|
||||
this.registerEvent('authentication', ({ store }, data) => {
|
||||
store.dispatch('auth/setWebSocketId', data.web_socket_id)
|
||||
})
|
||||
|
||||
this.registerEvent('group_created', ({ store }, data) => {
|
||||
store.dispatch('group/forceCreate', data.group)
|
||||
})
|
||||
|
||||
this.registerEvent('group_updated', ({ store }, data) => {
|
||||
const group = store.getters['group/get'](data.group_id)
|
||||
if (group !== undefined) {
|
||||
store.dispatch('group/forceUpdate', { group, values: data.group })
|
||||
}
|
||||
})
|
||||
|
||||
this.registerEvent('group_deleted', ({ store }, data) => {
|
||||
const group = store.getters['group/get'](data.group_id)
|
||||
if (group !== undefined) {
|
||||
store.dispatch('group/forceDelete', group)
|
||||
}
|
||||
})
|
||||
|
||||
this.registerEvent('application_created', ({ store }, data) => {
|
||||
store.dispatch('application/forceCreate', { data: data.application })
|
||||
})
|
||||
|
||||
this.registerEvent('application_updated', ({ store }, data) => {
|
||||
const application = store.getters['application/get'](data.application_id)
|
||||
if (application !== undefined) {
|
||||
store.dispatch('application/forceUpdate', {
|
||||
application,
|
||||
data: data.application,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.registerEvent('application_deleted', ({ store }, data) => {
|
||||
const application = store.getters['application/get'](data.application_id)
|
||||
if (application !== undefined) {
|
||||
store.dispatch('application/forceDelete', application)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default function (context, inject) {
|
||||
inject('realtime', new RealTimeHandler(context))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue