cbcore-ts
Version:
CBCore is a library to build web applications using pure Typescript.
760 lines (441 loc) • 21.3 kB
text/typescript
import { io, Socket } from "socket.io-client"
import { FIRST_OR_NIL, IF, IS, IS_NIL, IS_NOT, MAKE_ID, nil, NO, RETURNER, UIObject, YES } from "../../uicore-ts"
import { CBCore } from "./CBCore"
import {
CBSocketHandshakeInitMessage,
CBSocketHandshakeResponseMessage,
CBSocketMessage,
CBSocketMessageCompletionFunction,
CBSocketMessageHandlerFunction,
CBSocketMessageSendResponseFunction,
CBSocketMultipleMessage,
CBSocketMultipleMessagecompletionFunction,
CBSocketMultipleMessageObject,
SocketClientInterface
} from "./CBDataInterfaces"
import { CBSocketCallbackHolder } from "./CBSocketCallbackHolder"
declare interface CBSocketClientMessageToBeSent {
key: string;
message: any;
inResponseToMessage: CBSocketMessage<any>;
keepWaitingForResponses: boolean;
isBoundToUserWithID: string;
completionPolicy: string;
didSendFunction?: () => void;
completion: CBSocketMessageCompletionFunction;
}
declare interface CBSocketClientErrorMessage {
_isCBSocketErrorMessage: boolean;
messageData: any;
}
type FilterConditionally<Source, Condition> = Pick<Source, { [K in keyof Source]: Source[K] extends Condition ? K : never }[keyof Source]>;
export function IS_SOCKET_ERROR(object: any): object is CBSocketClientErrorMessage {
const result = (IS(object) && object._isCBSocketErrorMessage)
return result
}
export function IS_NOT_SOCKET_ERROR(object: any) {
return !IS_SOCKET_ERROR(object)
}
export class CBSocketClient extends UIObject {
_socket: Socket = io()
_isConnectionEstablished = NO
_collectMessagesToSendLater = NO
_messagesToBeSent: CBSocketClientMessageToBeSent[] = []
static _sharedInstance: CBSocketClient
_core: CBCore
_subscribedKeys: {
[x: string]: boolean
} = {}
_callbackHolder = new CBSocketCallbackHolder(this)
static responseMessageKey = "CBSocketResponseMessage"
static multipleMessageKey = "CBSocketMultipleMessage"
static disconnectionMessage: CBSocketClientErrorMessage = {
_isCBSocketErrorMessage: YES,
messageData: "Server disconnected"
}
constructor(core: CBCore) {
super()
this._core = core
this.socket.on("connect", () => {
console.log("Socket.io connected to server. clientID = ", this.socket, " socketID = ", this.socket)
const isInstanceIdentifierAllowed = localStorage.getItem("IsInstanceIdentifierAllowed") == "true"
let instanceIdentifier = IF(isInstanceIdentifierAllowed)(() =>
localStorage.getItem("InstanceIdentifier")
).ELSE(() =>
""
)
if (IS_NOT(instanceIdentifier) && isInstanceIdentifierAllowed) {
instanceIdentifier = MAKE_ID()
localStorage.setItem("InstanceIdentifier", instanceIdentifier!)
}
const handshakeMessage: CBSocketHandshakeInitMessage = {
accessToken: localStorage.getItem("CBUserAccessToken") ?? undefined,
userID: this._core.userProfile?._id,
inquiryAccessKey: undefined,
instanceIdentifier: instanceIdentifier!
}
this.socket.emit("CBSocketHandshakeInitMessage", {
identifier: MAKE_ID(),
messageData: handshakeMessage
})
})
this.socket.on(
"CBSocketHandshakeResponseMessage",
(message: CBSocketMessage<CBSocketHandshakeResponseMessage>) => {
this._isConnectionEstablished = message.messageData.accepted
if (!message.messageData.accepted) {
console.log("SocketIO connection failed.")
this._core.dialogViewShowerClass.alert(
"Failed to establish connection to server.",
() => {
}
)
}
else {
console.log("SocketIO connection handshake completed.")
this._callbackHolder = new CBSocketCallbackHolder(this, this._callbackHolder)
core.userProfile = message.messageData.userProfile ?? {}
this.sendUnsentMessages()
}
}
)
this.socket.on("disconnect", () => {
console.log("Socket.io disconnected from server. clientID = ", this.socket)
this._isConnectionEstablished = NO
this._callbackHolder.isValid = NO
this._callbackHolder.triggerDisconnectHandlers()
})
this.socket.on("CBPerformReconnect", (message?: string) => {
console.log("Performing socket reconnection.")
core.reloadSocketConnection()
if (message) {
this._core.dialogViewShowerClass.alert(message)
}
})
this._socket.on(
CBSocketClient.responseMessageKey,
(message: CBSocketMessage<any>) => {
this.didReceiveMessageForKey(CBSocketClient.responseMessageKey, message)
}
)
this._socket.on(
CBSocketClient.multipleMessageKey,
(message: CBSocketMessage<CBSocketMultipleMessageObject[]>) => {
console.log("Received " + message.messageData.length + " messages.")
this.didReceiveMessageForKey(CBSocketClient.multipleMessageKey, message)
}
)
}
get socket() {
return this._socket
}
cancelUnsentMessages(messagesToCancel: CBSocketClientMessageToBeSent[]) {
this._messagesToBeSent = this._messagesToBeSent.filter((
messageObject: CBSocketClientMessageToBeSent,
index: number,
array: CBSocketClientMessageToBeSent[]
) => !messagesToCancel.contains(messageObject))
}
sendUnsentMessages(receiveResponsesTogether = NO, completion?: CBSocketMultipleMessagecompletionFunction) {
if (!this._isConnectionEstablished || this._collectMessagesToSendLater) {
return
}
const groupedMessages: CBSocketMultipleMessageObject<any>[] = []
const didSendFunctions: (() => void)[] = []
this._messagesToBeSent.copy().forEach((messageToBeSentObject: CBSocketClientMessageToBeSent) => {
if (this._isConnectionEstablished) {
var message = messageToBeSentObject.message
if (IS_NOT(message)) {
message = ""
}
const identifier = MAKE_ID()
const completion = messageToBeSentObject.completion
const messageObject: CBSocketMessage<any> = {
messageData: message,
identifier: identifier,
keepWaitingForResponses: messageToBeSentObject.keepWaitingForResponses,
inResponseToIdentifier: messageToBeSentObject.inResponseToMessage.identifier,
completionPolicy: messageToBeSentObject.completionPolicy
}
const shouldSendMessage = this._callbackHolder.socketShouldSendMessage(
messageToBeSentObject.key,
messageObject,
messageToBeSentObject.completionPolicy,
completion
)
if (shouldSendMessage) {
groupedMessages.push({
key: messageToBeSentObject.key,
message: messageObject
})
}
didSendFunctions.push(messageToBeSentObject.didSendFunction!)
}
})
this._messagesToBeSent = []
if (IS_NOT(groupedMessages.length)) {
return
}
if (groupedMessages.length == 1) {
console.log("sending 1 unsent message.")
}
else {
console.log("Sending " + groupedMessages.length + " unsent messages.")
}
const messageObject: CBSocketMultipleMessage = {
messageData: groupedMessages,
identifier: MAKE_ID(),
completionPolicy: CBSocketClient.completionPolicy.all,
shouldGroupResponses: receiveResponsesTogether
}
//if (receiveResponsesTogether) {
this._callbackHolder.socketWillSendMultipleMessage(messageObject, completion)
//}
this.socket.emit(CBSocketClient.multipleMessageKey, messageObject)
didSendFunctions.forEach((didSendFunction, index, array) => {
didSendFunction()
})
}
static completionPolicy = {
"all": "all",
"allDifferent": "allDifferent",
"first": "first",
"last": "last",
"firstAndLast": "firstAndLast",
"firstAndLastIfDifferent": "firstAndLastIfDifferent",
"directOnly": "directOnly",
"firstOnly": "firstOnly",
"storedOrFirst": "storedOrFirst"
} as const
sendUserBoundMessageForKeyWithPolicy(
key: keyof SocketClientInterface,
message: any,
completionPolicy: keyof typeof CBSocketClient.completionPolicy,
completion?: CBSocketMessageCompletionFunction
) {
this._sendMessageForKey(key as string, message, undefined, NO, completionPolicy, YES, nil, completion)
}
sendUserBoundMessageForKey(
key: keyof SocketClientInterface,
message: any,
completion?: CBSocketMessageCompletionFunction
) {
this._sendMessageForKey(key as string, message, undefined, NO, undefined, YES, nil, completion)
}
sendMessageForKeyWithPolicy(
key: keyof SocketClientInterface,
message: any,
completionPolicy: keyof typeof CBSocketClient.completionPolicy,
completion?: CBSocketMessageCompletionFunction
) {
this._sendMessageForKey(key as string, message, undefined, NO, completionPolicy, NO, nil, completion)
}
sendMessageForKey(key: keyof SocketClientInterface, message: any, completion?: CBSocketMessageCompletionFunction) {
this._sendMessageForKey(key as string, message, undefined, NO, undefined, NO, nil, completion)
}
resultForMessageForKey(
key: keyof SocketClientInterface,
message: any,
completionPolicy?: keyof typeof CBSocketClient.completionPolicy,
isUserBound = NO
) {
const result = new Promise<{
responseMessage: any,
result: any,
errorResult: any,
respondWithMessage: CBSocketMessageSendResponseFunction
}>((resolve, reject) => {
this._sendMessageForKey(
key as string,
message,
undefined,
NO,
completionPolicy,
isUserBound,
nil,
(responseMessage, respondWithMessage) => resolve({
responseMessage: responseMessage,
result: IF(IS_NOT_SOCKET_ERROR(responseMessage))(() => responseMessage).ELSE(RETURNER(undefined)),
errorResult: IF(IS_SOCKET_ERROR(responseMessage))(() => responseMessage).ELSE(RETURNER(undefined)),
respondWithMessage: respondWithMessage
})
)
})
return result
}
_sendMessageForKey(
key: string,
message: any,
inResponseToMessage: CBSocketMessage<any> = {} as any,
keepMessageConnectionOpen = NO,
completionPolicy: keyof typeof CBSocketClient.completionPolicy = CBSocketClient.completionPolicy.directOnly,
isUserBound = NO,
didSendFunction: () => void = nil,
completion: CBSocketMessageCompletionFunction = nil
) {
if (IS_NIL(message)) {
message = ""
}
if (this._isConnectionEstablished && !this._collectMessagesToSendLater) {
const identifier = MAKE_ID()
const messageObject: CBSocketMessage<any> = {
messageData: message,
identifier: identifier,
keepWaitingForResponses: keepMessageConnectionOpen,
inResponseToIdentifier: inResponseToMessage.identifier,
completionPolicy: completionPolicy
}
const shouldSendMessage = this._callbackHolder.socketShouldSendMessage(
key,
messageObject,
completionPolicy,
completion
)
if (shouldSendMessage) {
this.socket.emit(key, messageObject)
}
didSendFunction()
}
else {
this._messagesToBeSent.push({
key: key,
message: message,
inResponseToMessage: inResponseToMessage,
keepWaitingForResponses: keepMessageConnectionOpen,
completionPolicy: completionPolicy,
isBoundToUserWithID: IF(isUserBound)(RETURNER(FIRST_OR_NIL(CBCore.sharedInstance.userProfile._id)))(),
didSendFunction: didSendFunction,
completion: completion
})
return this._messagesToBeSent.lastElement
}
}
sendMessagesAsGroup<FunctionReturnType extends object>(functionToCall: () => FunctionReturnType) {
const collectMessagesToSendLater = this._collectMessagesToSendLater
this._collectMessagesToSendLater = YES
var result = functionToCall()
this._collectMessagesToSendLater = collectMessagesToSendLater
this.sendUnsentMessages()
return result
}
sendAndReceiveMessagesAsGroup<FunctionReturnType extends object>(
functionToCall: () => FunctionReturnType,
completion?: CBSocketMultipleMessagecompletionFunction
) {
const collectMessagesToSendLater = this._collectMessagesToSendLater
this._collectMessagesToSendLater = YES
var result = functionToCall()
this._collectMessagesToSendLater = collectMessagesToSendLater
this.sendUnsentMessages(YES, completion)
return result
}
didReceiveMessageForKey(key: string, message: CBSocketMessage<any>) {
const sendResponseFunction: CBSocketMessageSendResponseFunction = function (
this: CBSocketClient,
responseMessage: any,
completion: CBSocketMessageCompletionFunction
) {
this._sendMessageForKey(
CBSocketClient.responseMessageKey,
responseMessage,
message,
NO,
undefined,
NO,
nil,
completion
)
}.bind(this) as any
sendResponseFunction.sendIntermediateResponse = function (
this: CBSocketClient,
updateMessage: any,
completion: CBSocketMessageCompletionFunction
) {
this._sendMessageForKey(
CBSocketClient.responseMessageKey,
updateMessage,
message,
YES,
undefined,
NO,
nil,
completion
)
}.bind(this)
const sendUserBoundResponseFunction: CBSocketMessageSendResponseFunction = function (
this: CBSocketClient,
responseMessage: any,
completion: CBSocketMessageCompletionFunction
) {
this._sendMessageForKey(
CBSocketClient.responseMessageKey,
responseMessage,
message,
NO,
undefined,
YES,
nil,
completion
)
}.bind(this) as any
sendUserBoundResponseFunction.sendIntermediateResponse = function (
this: CBSocketClient,
updateMessage: any,
completion: CBSocketMessageCompletionFunction
) {
this._sendMessageForKey(
CBSocketClient.responseMessageKey,
updateMessage,
message,
YES,
undefined,
YES,
nil,
completion
)
}.bind(this)
if (IS_SOCKET_ERROR(message.messageData)) {
console.log("CBSocketClient did receive error message.")
console.log(message.messageData)
}
this._callbackHolder.socketDidReceiveMessageForKey(key, message, sendResponseFunction)
}
addTargetForMessagesForKeys(keys: string[], handlerFunction: CBSocketMessageHandlerFunction) {
keys.forEach(function (this: CBSocketClient, key: string, index: number, array: string[]) {
this.addTargetForMessagesForKey(key, handlerFunction)
}.bind(this))
}
addTargetForMessagesForKey(key: string, handlerFunction: CBSocketMessageHandlerFunction) {
this._callbackHolder.registerHandler(key, handlerFunction)
if (IS_NOT(this._subscribedKeys[key])) {
this._socket.on(key, function (this: CBSocketClient, message: CBSocketMessage<any>) {
this.didReceiveMessageForKey(key, message)
}.bind(this))
this._subscribedKeys[key] = true
}
}
addTargetForOneMessageForKey(key: string, handlerFunction: CBSocketMessageHandlerFunction) {
this._callbackHolder.registerOnetimeHandler(key, handlerFunction)
if (IS_NOT(this._subscribedKeys[key])) {
this._socket.on(key, function (this: CBSocketClient, message: CBSocketMessage<any>) {
this.didReceiveMessageForKey(key, message)
}.bind(this))
this._subscribedKeys[key] = true
}
}
}
export const SocketClient: SocketClientInterface = new Proxy({ "name": "SocketClient" }, {
get(target, key) {
const result = (
messageData: any,
completionPolicy: string | undefined,
isUserBound: boolean | undefined
) => CBCore.sharedInstance.socketClient.resultForMessageForKey(
key as string,
messageData,
completionPolicy as any,
isUserBound
)
return result
}
}) as any