mirror of
https://gitlab.com/bramw/baserow.git
synced 2024-11-24 16:36:46 +00:00
420 lines
12 KiB
JavaScript
420 lines
12 KiB
JavaScript
import { isSecureURL } from '@baserow/modules/core/utils/string'
|
|
import { logoutAndRedirectToLogin } from '@baserow/modules/core/utils/auth'
|
|
|
|
export class RealTimeHandler {
|
|
constructor(context) {
|
|
this.context = context
|
|
this.socket = null
|
|
this.connected = false
|
|
this.reconnect = false
|
|
this.anonymous = false
|
|
this.reconnectTimeout = null
|
|
this.attempts = 0
|
|
this.events = {}
|
|
this.pages = []
|
|
this.subscribedToPages = true
|
|
this.lastToken = null
|
|
this.authenticationSuccess = true
|
|
this.registerCoreEvents()
|
|
}
|
|
|
|
/**
|
|
* Creates a new connection with to the web socket so that real time updates can be
|
|
* received.
|
|
*/
|
|
connect(reconnect = true, anonymous = false) {
|
|
this.reconnect = reconnect
|
|
this.anonymous = anonymous
|
|
|
|
const jwtToken = this.context.store.getters['auth/token']
|
|
const token = anonymous ? jwtToken || 'anonymous' : jwtToken
|
|
|
|
// If the user is already connected to the web socket, we don't have to do
|
|
// anything.
|
|
if (this.connected) {
|
|
return
|
|
}
|
|
|
|
// Stop connecting if we have already tried more than 10 times, if we do not have
|
|
// an authentication token or if the server has already responded with a failed
|
|
// authentication error and the token has not changed.
|
|
if (
|
|
this.attempts > 10 ||
|
|
token === null ||
|
|
(!this.authenticationSuccess && token === this.lastToken)
|
|
) {
|
|
this.context.store.dispatch('toast/setFailedConnecting', true)
|
|
return
|
|
}
|
|
|
|
this.lastToken = token
|
|
|
|
// The web socket url is the same as the PUBLIC_BACKEND_URL apart from the
|
|
// protocol.
|
|
const rawUrl = this.context.app.$config.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('toast/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.subscribedToPages) {
|
|
this.subscribeToPages()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.subscribedToPages = this.pages.length === 0
|
|
this.delayedReconnect()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If reconnecting is enabled then a timeout is created that will try to connect
|
|
* to the backend one more time.
|
|
*/
|
|
delayedReconnect() {
|
|
if (!this.reconnect) {
|
|
return
|
|
}
|
|
|
|
this.attempts++
|
|
this.context.store.dispatch('toast/setConnecting', true)
|
|
|
|
this.reconnectTimeout = setTimeout(
|
|
() => {
|
|
this.connect(true, this.anonymous)
|
|
},
|
|
// After the first try, we want to try again every 5 seconds.
|
|
this.attempts > 1 ? 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) {
|
|
const pageScope = {
|
|
page,
|
|
parameters,
|
|
}
|
|
|
|
if (
|
|
!this.pages.some(
|
|
(elem) => JSON.stringify(elem) === JSON.stringify(pageScope)
|
|
)
|
|
) {
|
|
this.pages.push(pageScope)
|
|
// If the client is already connected we can
|
|
// subscribe to updates for all pages.
|
|
if (this.connected) {
|
|
this.subscribeToPage(page, parameters)
|
|
} else {
|
|
this.subscribedToPages = false
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unsubscribes the client from a given page. The client will
|
|
* stop receiving updates related to that page.
|
|
*/
|
|
unsubscribe(page, parameters) {
|
|
this.pages = this.pages.filter(
|
|
(item) => JSON.stringify(item) !== JSON.stringify({ page, parameters })
|
|
)
|
|
this.socket.send(
|
|
JSON.stringify({
|
|
remove_page: page,
|
|
...parameters,
|
|
})
|
|
)
|
|
}
|
|
|
|
/*
|
|
* Subscribes the client to a new page if the client is
|
|
* connected.
|
|
*/
|
|
subscribeToPage(page, parameters) {
|
|
if (this.connected) {
|
|
this.socket.send(
|
|
JSON.stringify({
|
|
page: page === null ? '' : page,
|
|
...parameters,
|
|
})
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Requests real time updates for the list of pages that
|
|
* have been collected by the subscribe() call.
|
|
*/
|
|
subscribeToPages() {
|
|
if (this.subscribedToPages) {
|
|
return
|
|
}
|
|
|
|
for (const { page, parameters } of this.pages) {
|
|
this.subscribeToPage(page, parameters)
|
|
}
|
|
|
|
this.subscribedToPages = 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) {
|
|
this.socket.close()
|
|
}
|
|
|
|
this.context.store.dispatch('toast/setConnecting', false)
|
|
this.context.store.dispatch('toast/setFailedConnecting', false)
|
|
clearTimeout(this.reconnectTimeout)
|
|
this.reconnect = false
|
|
this.attempts = 0
|
|
this.connected = false
|
|
}
|
|
|
|
/**
|
|
* 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 workspaces 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)
|
|
|
|
// Store if the authentication was successful in order to prevent retries that
|
|
// will fail.
|
|
this.authenticationSuccess = data.success
|
|
})
|
|
|
|
this.registerEvent('user_data_updated', ({ store }, data) => {
|
|
store.dispatch('auth/forceUpdateUserData', data.user_data)
|
|
})
|
|
|
|
this.registerEvent('user_updated', ({ store }, data) => {
|
|
store.dispatch('workspace/forceUpdateWorkspaceUserAttributes', {
|
|
userId: data.user.id,
|
|
values: {
|
|
name: data.user.first_name,
|
|
},
|
|
})
|
|
})
|
|
|
|
this.registerEvent('user_deleted', ({ store }, data) => {
|
|
store.dispatch('workspace/forceUpdateWorkspaceUserAttributes', {
|
|
userId: data.user.id,
|
|
values: {
|
|
to_be_deleted: true,
|
|
},
|
|
})
|
|
})
|
|
|
|
this.registerEvent('user_restored', ({ store }, data) => {
|
|
store.dispatch('workspace/forceUpdateWorkspaceUserAttributes', {
|
|
userId: data.user.id,
|
|
values: {
|
|
to_be_deleted: false,
|
|
},
|
|
})
|
|
})
|
|
|
|
this.registerEvent('user_permanently_deleted', ({ store }, data) => {
|
|
store.dispatch('workspace/forceDeleteUser', {
|
|
userId: data.user_id,
|
|
})
|
|
})
|
|
|
|
this.registerEvent('group_created', ({ store }, data) => {
|
|
store.dispatch('workspace/forceCreate', data.workspace)
|
|
})
|
|
|
|
this.registerEvent('group_restored', ({ store }, data) => {
|
|
store.dispatch('workspace/forceCreate', data.workspace)
|
|
store.dispatch('application/forceCreateAll', data.applications)
|
|
})
|
|
|
|
this.registerEvent('group_updated', ({ store }, data) => {
|
|
const workspace = store.getters['workspace/get'](data.workspace_id)
|
|
if (workspace !== undefined) {
|
|
store.dispatch('workspace/forceUpdate', {
|
|
workspace,
|
|
values: data.workspace,
|
|
})
|
|
}
|
|
})
|
|
|
|
this.registerEvent('group_deleted', ({ store }, data) => {
|
|
const workspace = store.getters['workspace/get'](data.workspace_id)
|
|
if (workspace !== undefined) {
|
|
store.dispatch('workspace/forceDelete', workspace)
|
|
}
|
|
})
|
|
|
|
this.registerEvent('groups_reordered', ({ store }, data) => {
|
|
store.dispatch('workspace/forceOrder', data.workspace_ids)
|
|
})
|
|
|
|
this.registerEvent('group_user_added', ({ store }, data) => {
|
|
store.dispatch('workspace/forceAddWorkspaceUser', {
|
|
workspaceId: data.workspace_id,
|
|
values: data.workspace_user,
|
|
})
|
|
})
|
|
|
|
this.registerEvent('group_user_updated', ({ store }, data) => {
|
|
store.dispatch('workspace/forceUpdateWorkspaceUser', {
|
|
id: data.id,
|
|
workspaceId: data.workspace_id,
|
|
values: data.workspace_user,
|
|
})
|
|
})
|
|
|
|
this.registerEvent('group_user_deleted', ({ store }, data) => {
|
|
store.dispatch('workspace/forceDeleteWorkspaceUser', {
|
|
id: data.id,
|
|
workspaceId: data.workspace_id,
|
|
values: data.workspace_user,
|
|
})
|
|
})
|
|
|
|
this.registerEvent('application_created', ({ store }, data) => {
|
|
store.dispatch('application/forceCreate', 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)
|
|
}
|
|
})
|
|
|
|
this.registerEvent('applications_reordered', ({ store }, data) => {
|
|
const workspace = store.getters['workspace/get'](data.workspace_id)
|
|
if (workspace !== undefined) {
|
|
store.commit('application/ORDER_ITEMS', {
|
|
workspace,
|
|
order: data.order,
|
|
isHashed: true,
|
|
})
|
|
}
|
|
})
|
|
|
|
// invitations
|
|
this.registerEvent(
|
|
'workspace_invitation_updated_or_created',
|
|
({ store }, data) => {
|
|
store.dispatch(
|
|
'auth/forceUpdateOrCreateWorkspaceInvitation',
|
|
data.invitation
|
|
)
|
|
}
|
|
)
|
|
|
|
this.registerEvent('workspace_invitation_accepted', ({ store }, data) => {
|
|
store.dispatch('auth/forceAcceptWorkspaceInvitation', data.invitation)
|
|
})
|
|
|
|
this.registerEvent('workspace_invitation_rejected', ({ store }, data) => {
|
|
store.dispatch('auth/forceRejectWorkspaceInvitation', data.invitation)
|
|
})
|
|
|
|
// notifications
|
|
this.registerEvent('notifications_created', ({ store }, data) => {
|
|
store.dispatch('notification/forceCreateInBulk', {
|
|
notifications: data.notifications,
|
|
})
|
|
})
|
|
|
|
this.registerEvent('notifications_fetch_required', ({ store }, data) => {
|
|
store.dispatch('notification/forceRefetch', {
|
|
notificationsAdded: data.notifications_added,
|
|
})
|
|
})
|
|
|
|
this.registerEvent('notification_marked_as_read', ({ store }, data) => {
|
|
store.dispatch('notification/forceMarkAsRead', {
|
|
notification: data.notification,
|
|
})
|
|
})
|
|
|
|
this.registerEvent('all_notifications_marked_as_read', ({ store }) => {
|
|
store.dispatch('notification/forceMarkAllAsRead')
|
|
})
|
|
|
|
this.registerEvent('all_notifications_cleared', ({ store }) => {
|
|
store.dispatch('notification/forceClearAll')
|
|
})
|
|
|
|
this.registerEvent('force_disconnect', ({ store }) => {
|
|
this.reconnect = false
|
|
logoutAndRedirectToLogin(this.context.app.router, store, false, true)
|
|
})
|
|
}
|
|
}
|
|
|
|
export default function (context, inject) {
|
|
inject('realtime', new RealTimeHandler(context))
|
|
}
|