chore: handle notifications from websockets (#2472)
This commit is contained in:
7
packages/services/src/Domain/Api/WebSocketsEventData.ts
Normal file
7
packages/services/src/Domain/Api/WebSocketsEventData.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Either } from '@standardnotes/common'
|
||||
import { UserRolesChangedEventPayload, NotificationAddedForUserEventPayload } from '@standardnotes/domain-events'
|
||||
|
||||
export interface WebSocketsEventData {
|
||||
type: string
|
||||
payload: Either<UserRolesChangedEventPayload, NotificationAddedForUserEventPayload>
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum WebSocketsServiceEvent {
|
||||
UserRoleMessageReceived = 'WebSocketMessageReceived',
|
||||
NotificationAddedForUser = 'NotificationAddedForUser',
|
||||
}
|
||||
37
packages/services/src/Domain/Api/WebsocketsService.spec.ts
Normal file
37
packages/services/src/Domain/Api/WebsocketsService.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { WebSocketApiServiceInterface } from '@standardnotes/api'
|
||||
|
||||
import { WebSocketsService } from './WebsocketsService'
|
||||
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { StorageKey } from '../Storage/StorageKeys'
|
||||
|
||||
describe('webSocketsService', () => {
|
||||
const webSocketUrl = ''
|
||||
|
||||
let storageService: StorageServiceInterface
|
||||
let webSocketApiService: WebSocketApiServiceInterface
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
|
||||
const createService = () => {
|
||||
return new WebSocketsService(storageService, webSocketUrl, webSocketApiService, internalEventBus)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
storageService = {} as jest.Mocked<StorageServiceInterface>
|
||||
storageService.setValue = jest.fn()
|
||||
|
||||
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
|
||||
internalEventBus.publish = jest.fn()
|
||||
|
||||
webSocketApiService = {} as jest.Mocked<WebSocketApiServiceInterface>
|
||||
webSocketApiService.createConnectionToken = jest.fn().mockReturnValue({ token: 'foobar' })
|
||||
})
|
||||
|
||||
describe('setWebSocketUrl()', () => {
|
||||
it('saves url in local storage', () => {
|
||||
const webSocketUrl = 'wss://test-websocket'
|
||||
createService().setWebSocketUrl(webSocketUrl)
|
||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.WebSocketUrl, webSocketUrl)
|
||||
})
|
||||
})
|
||||
})
|
||||
111
packages/services/src/Domain/Api/WebsocketsService.ts
Normal file
111
packages/services/src/Domain/Api/WebsocketsService.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { isErrorResponse } from '@standardnotes/responses'
|
||||
import {
|
||||
DomainEventInterface,
|
||||
UserRolesChangedEvent,
|
||||
NotificationAddedForUserEvent,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { WebSocketApiServiceInterface } from '@standardnotes/api'
|
||||
import { WebSocketsServiceEvent } from './WebSocketsServiceEvent'
|
||||
import { StorageServiceInterface } from '../Storage/StorageServiceInterface'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { StorageKey } from '../Storage/StorageKeys'
|
||||
import { WebSocketsEventData } from './WebSocketsEventData'
|
||||
|
||||
export class WebSocketsService extends AbstractService<WebSocketsServiceEvent, WebSocketsEventData> {
|
||||
private webSocket?: WebSocket
|
||||
|
||||
constructor(
|
||||
private storageService: StorageServiceInterface,
|
||||
private webSocketUrl: string | undefined,
|
||||
private webSocketApiService: WebSocketApiServiceInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
}
|
||||
|
||||
public setWebSocketUrl(url: string | undefined): void {
|
||||
this.webSocketUrl = url
|
||||
this.storageService.setValue(StorageKey.WebSocketUrl, url)
|
||||
}
|
||||
|
||||
public loadWebSocketUrl(): void {
|
||||
const storedValue = this.storageService.getValue<string | undefined>(StorageKey.WebSocketUrl)
|
||||
this.webSocketUrl =
|
||||
storedValue ||
|
||||
this.webSocketUrl ||
|
||||
(
|
||||
window as {
|
||||
_websocket_url?: string
|
||||
}
|
||||
)._websocket_url
|
||||
}
|
||||
|
||||
async startWebSocketConnection(): Promise<void> {
|
||||
if (!this.webSocketUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
const webSocketConectionToken = await this.createWebSocketConnectionToken()
|
||||
if (webSocketConectionToken === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.webSocket = new WebSocket(`${this.webSocketUrl}?authToken=${webSocketConectionToken}`)
|
||||
this.webSocket.onmessage = this.onWebSocketMessage.bind(this)
|
||||
this.webSocket.onclose = this.onWebSocketClose.bind(this)
|
||||
} catch (e) {
|
||||
console.error('Error starting WebSocket connection', e)
|
||||
}
|
||||
}
|
||||
|
||||
public closeWebSocketConnection(): void {
|
||||
this.webSocket?.close()
|
||||
}
|
||||
|
||||
private onWebSocketMessage(messageEvent: MessageEvent) {
|
||||
const eventData: DomainEventInterface = JSON.parse(messageEvent.data)
|
||||
switch (eventData.type) {
|
||||
case 'USER_ROLES_CHANGED':
|
||||
void this.notifyEvent(WebSocketsServiceEvent.UserRoleMessageReceived, eventData as UserRolesChangedEvent)
|
||||
break
|
||||
case 'NOTIFICATION_ADDED_FOR_USER':
|
||||
void this.notifyEvent(
|
||||
WebSocketsServiceEvent.NotificationAddedForUser,
|
||||
eventData as NotificationAddedForUserEvent,
|
||||
)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private onWebSocketClose() {
|
||||
this.webSocket = undefined
|
||||
}
|
||||
|
||||
private async createWebSocketConnectionToken(): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await this.webSocketApiService.createConnectionToken()
|
||||
if (isErrorResponse(response)) {
|
||||
console.error(response.data.error)
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
return response.data.token
|
||||
} catch (error) {
|
||||
console.error('Caught error:', (error as Error).message)
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
super.deinit()
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.webSocketApiService as unknown) = undefined
|
||||
this.closeWebSocketConnection()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NotificationServerHash } from '@standardnotes/responses'
|
||||
import { NotificationAddedForUserEvent } from '@standardnotes/domain-events'
|
||||
import { SyncEvent, SyncEventReceivedNotificationsData } from '../Event/SyncEvent'
|
||||
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
|
||||
@@ -6,6 +7,7 @@ import { InternalEventInterface } from '../Internal/InternalEventInterface'
|
||||
import { AbstractService } from '../Service/AbstractService'
|
||||
import { NotificationServiceEventPayload, NotificationServiceEvent } from './NotificationServiceEvent'
|
||||
import { NotificationPayload } from '@standardnotes/domain-core'
|
||||
import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent'
|
||||
|
||||
export class NotificationService
|
||||
extends AbstractService<NotificationServiceEvent, NotificationServiceEventPayload>
|
||||
@@ -18,8 +20,13 @@ export class NotificationService
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === SyncEvent.ReceivedNotifications) {
|
||||
return this.handleReceivedNotifications(event.payload as SyncEventReceivedNotificationsData)
|
||||
switch (event.type) {
|
||||
case SyncEvent.ReceivedNotifications:
|
||||
return this.handleReceivedNotifications(event.payload as SyncEventReceivedNotificationsData)
|
||||
case WebSocketsServiceEvent.NotificationAddedForUser:
|
||||
return this.handleReceivedNotifications([(event as NotificationAddedForUserEvent).payload.notification])
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ export * from './Api/ApiServiceEventData'
|
||||
export * from './Api/LegacyApiServiceInterface'
|
||||
export * from './Api/MetaReceivedData'
|
||||
export * from './Api/SessionRefreshedData'
|
||||
export * from './Api/WebSocketsEventData'
|
||||
export * from './Api/WebsocketsService'
|
||||
export * from './Api/WebSocketsServiceEvent'
|
||||
export * from './Application/AppGroupManagedApplication'
|
||||
export * from './Application/ApplicationInterface'
|
||||
export * from './Application/ApplicationStage'
|
||||
|
||||
Reference in New Issue
Block a user