@stadium-ws/core
Version:
Build scalable realtime applications.
301 lines (245 loc) • 6.63 kB
text/typescript
/**
* TODO:
* - Add socket authentication (which is required!)
* - createUser
* - addUserToChannel
* - updateChannel
* - onEvent?
* - onChannelEvent?
* - getChannelMessages
* - getChannel
* - createChannelEvent
* - onNotification?
* - registerUserDevice? for push notifications
*/
import EventEmitter from 'eventemitter3'
import type { SocketMessage } from './Connection'
import Connection from './Connection'
import Requester from './Requester'
import type {
Channel,
ReplyGetChannels,
User,
ReplyGetChannelUsers,
Event,
CreateEvent,
QueryGetChannelEvents,
ReplyGetChannelEvents,
UpdateUser,
ReplyGetUserRoles,
ReplyGetUsers,
QueryGetUserRoles,
QueryGetUsers,
QueryGetChannels
} from './types'
import type { EventName } from './utils'
import { getEventName } from './utils'
interface IStadiumConfig {
clientId?: string
clientSecret?: string
apiUrl?: string
gatewayUrl?: string
}
export class Stadium {
private apiUrl: string
private gatewayUrl: string
private readonly config: IStadiumConfig
private requester: Requester
private accessToken?: string
private connection?: Connection
private emitter?: EventEmitter
constructor (config: IStadiumConfig = {}) {
this.apiUrl = config.apiUrl || 'https://api.stadium.ws'
this.gatewayUrl = config.gatewayUrl || 'wss://gateway.stadium.ws'
this.config = config
this.requester = new Requester(this.apiUrl)
}
public on (event: EventName, listener: (...args: any[]) => void) {
if (!this.emitter) {
this.emitter = new EventEmitter()
}
this.emitter.on(event, listener)
}
public once (event: EventName, listener: (...args: any[]) => void) {
if (!this.emitter) {
this.emitter = new EventEmitter()
}
this.emitter.once(event, listener)
}
public off (event: EventName, listener: (...args: any[]) => void) {
if (!this.emitter) {
return
}
this.emitter.off(event, listener)
}
public removeAllListeners () {
if (!this.emitter) {
return
}
this.emitter.removeAllListeners()
}
public async createUser ({
userRoleId,
displayName,
meta
}: {
userRoleId?: string
displayName?: string,
meta?: any
}): Promise<User> {
await this.ensureAccessToken()
return this.requester.request({
urlSegment: 'users',
method: 'POST',
body: {
userRoleId,
displayName,
meta
}
})
}
public setUserToken (token: string) {
this.accessToken = token
this.requester.setTokenHeader(this.accessToken)
}
public async getMe (): Promise<User> {
const res = await this.requester.request<{
user: User
}>({
urlSegment: 'oauth/token'
})
return res.user
}
public async getChannel (channelId: string): Promise<Channel> {
await this.ensureAccessToken()
return this.requester.request({
urlSegment: `channels/${channelId}`
})
}
public async getChannelUsers (channelId: string): Promise<ReplyGetChannelUsers> {
await this.ensureAccessToken()
return this.requester.request({
urlSegment: `channels/${channelId}/users`
})
}
public async createEvent (options: CreateEvent): Promise<Event> {
await this.ensureAccessToken()
return this.requester.request({
urlSegment: 'events',
method: 'POST',
body: options
})
}
public async getChannelEvents (channelId: string, options: QueryGetChannelEvents): Promise<ReplyGetChannelEvents> {
await this.ensureAccessToken()
const query: QueryGetChannelEvents = {
from: options?.from,
limit: options?.limit,
direction: options?.direction,
type: options?.type
}
return this.requester.request({
urlSegment: `channels/${channelId}/events`,
query
})
}
public async getUserRoles (options: QueryGetUserRoles): Promise<ReplyGetUserRoles> {
await this.ensureAccessToken()
const query: QueryGetUserRoles = {
from: options?.from,
limit: options?.limit,
direction: options?.direction
}
return this.requester.request({
urlSegment: 'user-roles',
query
})
}
public async getUsers (options: QueryGetUsers): Promise<ReplyGetUsers> {
await this.ensureAccessToken()
const query: QueryGetUserRoles = {
from: options?.from,
limit: options?.limit,
direction: options?.direction
}
return this.requester.request({
urlSegment: 'users',
query
})
}
public async getChannels (options: QueryGetChannels): Promise<ReplyGetChannels> {
await this.ensureAccessToken()
const query: QueryGetChannels = {
from: options?.from,
limit: options?.limit,
direction: options?.direction
}
return this.requester.request({
urlSegment: 'channels',
query
})
}
private async ensureAccessToken () {
if (this.accessToken) {
return
}
const accessTokenResponse = await this.requester.request<{
access_token: string
}>({
urlSegment: 'oauth/token',
method: 'POST',
body: {
grant_type: 'client_credentials',
client_id: this.config.clientId,
client_secret: this.config.clientSecret
}
})
if (!accessTokenResponse?.access_token) {
throw new Error('Could not authenticate with Stadium')
}
this.accessToken = accessTokenResponse.access_token
this.requester.setTokenHeader(this.accessToken)
}
public updateUser = async (userId: string, options: UpdateUser): Promise<User> => {
await this.ensureAccessToken()
const body: any = {}
if (options.displayName) {
body.displayName = options.displayName
}
if (options.isOnline) {
body.isOnline = options.isOnline
}
if (options.meta) {
body.meta = options.meta
}
if (options.userRoleId) {
body.userRoleId = options.userRoleId
}
return this.requester.request({
urlSegment: `users/${userId}`,
method: 'PUT',
body
})
}
private onEvent = (event: SocketMessage) => {
const eventName = getEventName(event.type)
this.emitter?.emit(eventName, event.data)
}
public async connect () {
this.connection = new Connection({
baseUrl: this.gatewayUrl
})
this.emitter = new EventEmitter()
await this.connection.connect({
token: this.accessToken!,
onEvent: this.onEvent
})
}
public async disconnect () {
if (!this.connection) {
return
}
this.emitter?.removeAllListeners()
return this.connection.disconnect()
}
}