cbcore-ts
Version:
CBCore is a library to build web applications using pure Typescript.
872 lines (447 loc) • 27.3 kB
text/typescript
import objectHash from "object-hash"
import { FIRST, IS, IS_NOT, nil, NO, UIObject, YES } from "../../uicore-ts"
import {
CBSocketMessage,
CBSocketMessageCompletionFunction,
CBSocketMessageHandlerFunction, CBSocketMessageSendResponseFunction, CBSocketMultipleMessage,
CBSocketMultipleMessagecompletionFunction, CBSocketMultipleMessageObject
} from "./CBDataInterfaces"
import { CBSocketClient } from "./CBSocketClient"
interface CBSocketCallbackHolderMessageDescriptor {
key: string;
message: {
identifier: string;
inResponseToIdentifier?: string;
keepWaitingForResponses?: boolean;
}
sentAtTime: number;
//completionTriggered: boolean;
messageDataHash: string;
responseDataHash?: string;
mainResponseReceived: boolean;
anyMainResponseReceived: boolean;
completionPolicy: string;
completionFunction: CBSocketMessageCompletionFunction;
}
interface CBSocketCallbackHolderStoredResponseObject {
messageKey: string;
messageData: any;
messageDataHash: string;
}
export class CBSocketCallbackHolder extends UIObject {
messageDescriptors: {
[x: string]: CBSocketCallbackHolderMessageDescriptor[]
} = {}
handlers: {
[x: string]: CBSocketMessageHandlerFunction[]
} = {}
onetimeHandlers: {
[x: string]: CBSocketMessageHandlerFunction[]
} = {}
keysForIdentifiers: {
[x: string]: string
} = {}
isValid = YES
_storeableResponseKeys: string[] = []
_storedResponseHashesDictionary: {
[x: string]: {
hash: string,
validityDate: number
}
} = {}
_verifiedResponseHashesDictionary: {
[x: string]: boolean
} = {}
_socketClient: CBSocketClient
constructor(socketClient: CBSocketClient, previousCallbackHolder?: CBSocketCallbackHolder) {
super()
this._socketClient = socketClient
if (IS(previousCallbackHolder)) {
this.handlers = previousCallbackHolder.handlers
this._verifiedResponseHashesDictionary = previousCallbackHolder._verifiedResponseHashesDictionary
}
}
triggerDisconnectHandlers() {
this.messageDescriptors.forEach(function (descriptor: CBSocketCallbackHolderMessageDescriptor, key: string) {
if (descriptor.mainResponseReceived) {
descriptor.completionFunction(CBSocketClient.disconnectionMessage, nil)
}
})
}
registerHandler(key: string, handlerFunction: CBSocketMessageHandlerFunction) {
if (!this.handlers[key]) {
this.handlers[key] = []
}
this.handlers[key].push(handlerFunction)
}
registerOnetimeHandler(key: string, handlerFunction: CBSocketMessageHandlerFunction) {
if (!this.onetimeHandlers[key]) {
this.onetimeHandlers[key] = []
}
this.onetimeHandlers[key].push(handlerFunction)
}
get storedResponseHashesDictionary() {
if (IS_NOT(this._storedResponseHashesDictionary)) {
this._storedResponseHashesDictionary = JSON.parse(localStorage["CBSocketResponseHashesDictionary"] || "{}")
}
return this._storedResponseHashesDictionary
}
storedResponseHashObjectForKey(requestKey: string, requestDataHash: string) {
const localStorageKey = this.keyForRequestKeyAndRequestDataHash(requestKey, requestDataHash)
const hashObject = this.storedResponseHashesDictionary[localStorageKey]
const result = FIRST(hashObject, {} as any)
return result
}
storedResponseForKey(requestKey: string, requestDataHash: string) {
const localStorageKey = this.keyForRequestKeyAndRequestDataHash(requestKey, requestDataHash)
const storedObject = JSON.parse(localStorage[localStorageKey] || "{}")
return storedObject.responseMessageData
}
keyForRequestKeyAndRequestDataHash(requestKey: string, requestDataHash: string) {
const result = "_CBSCH_LS_key_" + requestKey + "_" + requestDataHash
return result
}
storeResponse(
requestKey: string,
requestDataHash: string,
responseMessage: CBSocketMessage<any>,
responseDataHash: string
) {
if (!responseMessage.canBeStoredAsResponse ||
(IS_NOT(responseMessage.messageData) && IS_NOT(responseMessage.messageDataHash))) {
return
}
const localStorageKey = this.keyForRequestKeyAndRequestDataHash(requestKey, requestDataHash)
var validityDate: number
if (responseMessage.responseValidityDuration) {
validityDate = Date.now() + responseMessage.responseValidityDuration
}
const storedResponseHashesDictionary = this.storedResponseHashesDictionary
storedResponseHashesDictionary[localStorageKey] = {
hash: responseDataHash,
validityDate: validityDate!
}
this.saveInLocalStorage(localStorageKey, {
responseMessageData: responseMessage.messageData,
responseHash: responseDataHash
})
this.saveStoredResponseHashesDictionary(storedResponseHashesDictionary)
}
private saveStoredResponseHashesDictionary(storedResponseHashesDictionary: { [x: string]: { hash: string; validityDate: number; }; }) {
this.saveInLocalStorage("CBSocketResponseHashesDictionary", storedResponseHashesDictionary)
}
saveInLocalStorage(key: string, object: any) {
const stringToSave = JSON.stringify(object)
if (stringToSave != localStorage[key]) {
localStorage[key] = stringToSave
}
}
socketShouldSendMessage(
key: string,
message: CBSocketMessage<any>,
completionPolicy: string,
completionFunction: CBSocketMessageCompletionFunction
) {
var result = YES
var triggerStoredResponseImmediately = NO
const messageDataHash = objectHash(message.messageData || nil)
const descriptorKey = "socketMessageDescriptor_" + key + messageDataHash
this.messageDescriptors[descriptorKey] = (this.messageDescriptors[descriptorKey] || [])
const hashObject = this.storedResponseHashObjectForKey(key, messageDataHash)
message.storedResponseHash = hashObject.hash
if (completionPolicy == CBSocketClient.completionPolicy.first) {
const descriptorsForKey = (this.messageDescriptors[descriptorKey] || [])
const matchingDescriptor = descriptorsForKey.find(function (descriptor, index, array) {
return (descriptor.messageDataHash == messageDataHash)
})
if (matchingDescriptor) {
result = NO
}
}
if (completionPolicy == CBSocketClient.completionPolicy.storedOrFirst) {
const descriptorsForKey = (this.messageDescriptors[descriptorKey] || [])
const matchingDescriptor = descriptorsForKey.find(function (descriptor, index, array) {
return (descriptor.messageDataHash == messageDataHash)
})
const storedResponse = IS(message.storedResponseHash)
if (matchingDescriptor ||
(storedResponse && this._verifiedResponseHashesDictionary[message.storedResponseHash!])) {
result = NO
triggerStoredResponseImmediately = YES
}
}
if (completionPolicy == CBSocketClient.completionPolicy.firstOnly) {
const descriptorsForKey = (this.messageDescriptors[descriptorKey] || [])
const matchingDescriptor = descriptorsForKey.find(function (descriptor, index, array) {
return (descriptor.messageDataHash == messageDataHash)
})
if (matchingDescriptor) {
return NO
}
}
if (hashObject && hashObject.hash && hashObject.validityDate && message.storedResponseHash &&
this._verifiedResponseHashesDictionary[message.storedResponseHash] && hashObject.validityDate >
Date.now()) {
result = NO
triggerStoredResponseImmediately = YES
}
if (IS(completionFunction)) {
this.messageDescriptors[descriptorKey].push({
key: key,
message: {
identifier: message.identifier,
inResponseToIdentifier: message.inResponseToIdentifier,
keepWaitingForResponses: message.keepWaitingForResponses
},
sentAtTime: Date.now(),
//completionTriggered: NO,
messageDataHash: messageDataHash,
mainResponseReceived: NO,
anyMainResponseReceived: NO,
completionPolicy: completionPolicy,
completionFunction: completionFunction
})
this.keysForIdentifiers[message.identifier] = descriptorKey
}
if (triggerStoredResponseImmediately) {
this.socketDidReceiveMessageForKey(
CBSocketClient.responseMessageKey,
{
identifier: nil,
messageData: nil,
completionPolicy: CBSocketClient.completionPolicy.directOnly,
inResponseToIdentifier: message.identifier,
useStoredResponse: YES
},
nil
)
}
return result
}
static defaultMultipleMessagecompletionFunction(responseMessages: any[], callcompletionFunctions: () => void) {
callcompletionFunctions()
}
socketWillSendMultipleMessage(
messageToSend: CBSocketMultipleMessage,
completionFunction: CBSocketMultipleMessagecompletionFunction = CBSocketCallbackHolder.defaultMultipleMessagecompletionFunction
) {
const key = CBSocketClient.multipleMessageKey
const messageDataHash = objectHash(messageToSend.messageData || nil)
const descriptorKey = "socketMessageDescriptor_" + key + messageDataHash
this.messageDescriptors[descriptorKey] = (this.messageDescriptors[descriptorKey] || [])
messageToSend.storedResponseHash = this.storedResponseHashObjectForKey(key, messageDataHash).hash
this.messageDescriptors[descriptorKey].push({
key: key,
message: {
identifier: messageToSend.identifier,
inResponseToIdentifier: messageToSend.inResponseToIdentifier,
keepWaitingForResponses: messageToSend.keepWaitingForResponses
},
sentAtTime: Date.now(),
//completionTriggered: NO,
messageDataHash: messageDataHash,
mainResponseReceived: NO,
anyMainResponseReceived: NO,
completionPolicy: CBSocketClient.completionPolicy.directOnly,
completionFunction: function (
this: CBSocketCallbackHolder,
responseMessage: CBSocketMultipleMessageObject[],
respondWithMessage: any
) {
completionFunction(
responseMessage.map(function (messageObject, index, array) {
return messageObject.message.messageData
}),
function (this: CBSocketCallbackHolder) {
//console.log("Received multiple message response with length of " + responseMessage.length + ".");
// Call all completion functions
responseMessage.forEach(function (
this: CBSocketCallbackHolder,
messageObject: CBSocketMultipleMessageObject,
index: number,
array: CBSocketMultipleMessageObject[]
) {
this._socketClient.didReceiveMessageForKey(messageObject.key, messageObject.message)
}.bind(this))
}.bind(this)
)
}.bind(this)
})
this.keysForIdentifiers[messageToSend.identifier] = descriptorKey
}
socketDidReceiveMessageForKey(
key: string,
message: CBSocketMessage<any>,
sendResponseFunction: CBSocketMessageSendResponseFunction
) {
if (!this.isValid) {
return
}
// Call static handlers
if (this.handlers[key]) {
this.handlers[key].forEach(function (
this: CBSocketCallbackHolder,
handler: CBSocketMessageHandlerFunction,
index: any,
array: any
) {
handler(message.messageData, sendResponseFunction)
}.bind(this))
}
if (this.onetimeHandlers[key]) {
this.onetimeHandlers[key].forEach(function (
this: CBSocketCallbackHolder,
handler: CBSocketMessageHandlerFunction
) {
handler(message.messageData, sendResponseFunction)
}.bind(this))
delete this.onetimeHandlers[key]
}
// Temporary response handlers are evaluated here
if (message.inResponseToIdentifier &&
(CBSocketClient.responseMessageKey == key || CBSocketClient.multipleMessageKey == key)) {
// Find descriptors for the key of the message that is being responded to
const descriptorKey = this.keysForIdentifiers[message.inResponseToIdentifier]
const descriptorsForKey = (this.messageDescriptors[descriptorKey] || [])
// Find response data hash to check for differences
const responseDataHash = message.messageDataHash
// Remove identifier from dictionary
if (!message.keepWaitingForResponses) {
delete this.keysForIdentifiers[message.inResponseToIdentifier]
delete this.messageDescriptors[descriptorKey]
}
// Function to call completion function
const callCompletionFunction = (descriptor: CBSocketCallbackHolderMessageDescriptor, storedResponseCondition = NO) => {
var messageData = message.messageData
if (message.useStoredResponse && storedResponseCondition) {
messageData = this.storedResponseForKey(descriptor.key, descriptor.messageDataHash)
const responseHash = this.storedResponseHashObjectForKey(
descriptor.key,
descriptor.messageDataHash
).hash
const localStorageKey = this.keyForRequestKeyAndRequestDataHash(
descriptor.key,
descriptor.messageDataHash
)
if (message.responseValidityDuration && this.storedResponseHashesDictionary[localStorageKey]) {
this.storedResponseHashesDictionary[localStorageKey].validityDate = Date.now() +
message.responseValidityDuration
this.saveStoredResponseHashesDictionary(this.storedResponseHashesDictionary)
}
this._verifiedResponseHashesDictionary[responseHash] = YES
console.log("Using stored response.")
}
// Call completionFunction and set response data hash
descriptor.completionFunction(messageData, sendResponseFunction)
descriptor.responseDataHash = responseDataHash
}
descriptorsForKey.copy().forEach(function (
this: CBSocketCallbackHolder,
descriptor: CBSocketCallbackHolderMessageDescriptor,
index: number,
array: CBSocketCallbackHolderMessageDescriptor[]
) {
if ((descriptor.completionPolicy == CBSocketClient.completionPolicy.directOnly &&
descriptor.message.identifier == message.inResponseToIdentifier) || descriptor.completionPolicy ==
CBSocketClient.completionPolicy.first || descriptor.completionPolicy ==
CBSocketClient.completionPolicy.firstOnly || descriptor.completionPolicy ==
CBSocketClient.completionPolicy.storedOrFirst) {
// Calling completion function and removing descriptor
if (!message.keepWaitingForResponses) {
this.storeResponse(descriptor.key, descriptor.messageDataHash, message, responseDataHash!)
descriptorsForKey.removeElement(descriptor)
sendResponseFunction.respondingToMainResponse = YES
}
callCompletionFunction(descriptor, !message.keepWaitingForResponses)
}
else if (descriptor.completionPolicy == CBSocketClient.completionPolicy.all) {
// Calling completion function
callCompletionFunction(descriptor, !message.keepWaitingForResponses)
// Marking descriptor as having been responded to
if (!message.keepWaitingForResponses) {
if (message.inResponseToIdentifier == descriptor.message.identifier) {
sendResponseFunction.respondingToMainResponse = YES
descriptor.mainResponseReceived = YES
descriptorsForKey.removeElement(descriptor)
}
descriptor.anyMainResponseReceived = YES
}
}
else if (descriptor.completionPolicy == CBSocketClient.completionPolicy.allDifferent) {
// Calling completionFunction if messageData is different from previous
if (descriptor.responseDataHash != responseDataHash) {
callCompletionFunction(descriptor, !message.keepWaitingForResponses)
}
// Marking descriptor as having been responded to
if (!message.keepWaitingForResponses) {
if (message.inResponseToIdentifier == descriptor.message.identifier) {
sendResponseFunction.respondingToMainResponse = YES
descriptor.mainResponseReceived = YES
descriptorsForKey.removeElement(descriptor)
}
descriptor.anyMainResponseReceived = YES
}
}
else if (descriptor.completionPolicy == CBSocketClient.completionPolicy.last &&
descriptor.message.identifier == message.inResponseToIdentifier) {
if (!message.keepWaitingForResponses) {
// Marking descriptor as having been responded to
descriptor.mainResponseReceived = YES
descriptor.anyMainResponseReceived = YES
sendResponseFunction.respondingToMainResponse = YES
}
else {
descriptor.completionFunction(message.messageData, sendResponseFunction)
}
}
else if (descriptor.completionPolicy == CBSocketClient.completionPolicy.firstAndLast ||
descriptor.completionPolicy == CBSocketClient.completionPolicy.firstAndLastIfDifferent) {
if (!message.keepWaitingForResponses) {
// Only calling completionFunction once as a first response call
if (!descriptor.anyMainResponseReceived) {
callCompletionFunction(descriptor, !message.keepWaitingForResponses)
}
// Marking descriptor as having been responded to
if (descriptor.message.identifier == message.inResponseToIdentifier) {
descriptor.mainResponseReceived = YES
sendResponseFunction.respondingToMainResponse = YES
}
descriptor.anyMainResponseReceived = YES
}
else if (descriptor.message.identifier == message.inResponseToIdentifier &&
message.keepWaitingForResponses) {
descriptor.completionFunction(message.messageData, sendResponseFunction)
}
}
}.bind(this))
// Last message completion policies
const allResponsesReceived = descriptorsForKey.allMatch(function (descriptorObject, index, array) {
return descriptorObject.mainResponseReceived
})
descriptorsForKey.copy().forEach(function (
this: CBSocketCallbackHolder,
descriptor: CBSocketCallbackHolderMessageDescriptor,
index: number,
array: CBSocketCallbackHolderMessageDescriptor[]
) {
if ((descriptor.completionPolicy == CBSocketClient.completionPolicy.last ||
descriptor.completionPolicy == CBSocketClient.completionPolicy.firstAndLast) &&
allResponsesReceived && !message.keepWaitingForResponses) {
// Calling completionFunction
callCompletionFunction(descriptor, !message.keepWaitingForResponses)
// Cleaning up
descriptorsForKey.removeElement(descriptor)
}
else if (descriptor.completionPolicy == CBSocketClient.completionPolicy.firstAndLastIfDifferent &&
allResponsesReceived && !message.keepWaitingForResponses) {
// Calling completionFunction if needed
if (descriptor.responseDataHash != responseDataHash) {
callCompletionFunction(descriptor, !message.keepWaitingForResponses)
}
// Cleaning up
descriptorsForKey.removeElement(descriptor)
}
}.bind(this))
}
}
}