|
@@ -0,0 +1,284 @@
|
|
|
+type ActionHandler = {
|
|
|
+ [key: string]: [Function, any]
|
|
|
+}
|
|
|
+
|
|
|
+type CallbackCapabilities = {
|
|
|
+ [key: number]: PromiseCapability<any>
|
|
|
+}
|
|
|
+
|
|
|
+interface PromiseCapability<T> {
|
|
|
+ promise: Promise<T>
|
|
|
+ resolve: (value: T | PromiseLike<T>) => void
|
|
|
+ reject: (reason?: any) => void
|
|
|
+}
|
|
|
+
|
|
|
+class MessageHandler {
|
|
|
+ name: string
|
|
|
+ comObj: Window | Worker
|
|
|
+ callbackId: number
|
|
|
+ postMessageTransfers: boolean
|
|
|
+ callbacksCapabilities: CallbackCapabilities
|
|
|
+ actionHandler: ActionHandler
|
|
|
+ actionHandlerAsync: ActionHandler
|
|
|
+ nextAsync: Function | null
|
|
|
+ msgHandler: (event: MessageEvent) => void
|
|
|
+
|
|
|
+ constructor(name: string, comObj: Window | Worker) {
|
|
|
+ this.name = name
|
|
|
+ this.comObj = comObj
|
|
|
+ this.callbackId = 1
|
|
|
+ this.postMessageTransfers = true
|
|
|
+ this.callbacksCapabilities = {}
|
|
|
+ this.actionHandler = Object.create(null)
|
|
|
+ this.actionHandlerAsync = {}
|
|
|
+ this.nextAsync = null
|
|
|
+
|
|
|
+ this.actionHandler.console_log = [(k: any) => console.log(k)]
|
|
|
+ this.actionHandler.console_error = [(k: any) => console.error(k)]
|
|
|
+ this.actionHandler.workerLoaded = [() => {}]
|
|
|
+
|
|
|
+ this.msgHandler = this.handleMessage.bind(this)
|
|
|
+ comObj.addEventListener("message", this.msgHandler)
|
|
|
+ }
|
|
|
+
|
|
|
+ on(actionName: string, handler: Function, context?: any) {
|
|
|
+ if (this.actionHandler[actionName]) {
|
|
|
+ throw new Error(`There is already an actionName called "${actionName}"`)
|
|
|
+ }
|
|
|
+ this.actionHandler[actionName] = [handler, context]
|
|
|
+ }
|
|
|
+
|
|
|
+ clearActionHandlers() {
|
|
|
+ this.actionHandler = {}
|
|
|
+ this.comObj.removeEventListener("message", this.msgHandler)
|
|
|
+ }
|
|
|
+
|
|
|
+ reset() {
|
|
|
+ this.clearActionHandlers()
|
|
|
+ if (this.comObj.reset) {
|
|
|
+ this.comObj.reset()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ replace(actionName: string, handler: Function, context?: any) {
|
|
|
+ this.actionHandler[actionName] = [handler, context]
|
|
|
+ }
|
|
|
+
|
|
|
+ onAsync(actionName: string, handler: Function, context?: any) {
|
|
|
+ if (this.actionHandlerAsync[actionName]) {
|
|
|
+ console.error(`There is already an actionName called "${actionName}"`)
|
|
|
+ }
|
|
|
+ this.actionHandlerAsync[actionName] = [handler, context]
|
|
|
+ }
|
|
|
+
|
|
|
+ replaceAsync(actionName: string, handler: Function, context?: any) {
|
|
|
+ if (this.actionHandler[actionName]) {
|
|
|
+ delete this.actionHandler[actionName]
|
|
|
+ }
|
|
|
+ this.actionHandlerAsync[actionName] = [handler, context]
|
|
|
+ }
|
|
|
+
|
|
|
+ onNextAsync(handler: Function) {
|
|
|
+ this.nextAsync = handler
|
|
|
+ }
|
|
|
+
|
|
|
+ send(action: string, data: any) {
|
|
|
+ this.postMessage({ action, data })
|
|
|
+ }
|
|
|
+
|
|
|
+ getNextId(): number {
|
|
|
+ return this.callbackId++
|
|
|
+ }
|
|
|
+
|
|
|
+ sendWithPromise(action: string, data: any, priority?: number): Promise<any> {
|
|
|
+ const callbackId = this.getNextId()
|
|
|
+ const message = { action, data, callbackId, priority }
|
|
|
+ const capability = this.createPromiseCapability()
|
|
|
+ this.callbacksCapabilities[callbackId] = capability
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.postMessage(message)
|
|
|
+ } catch (error) {
|
|
|
+ capability.reject(error)
|
|
|
+ }
|
|
|
+
|
|
|
+ return capability.promise
|
|
|
+ }
|
|
|
+
|
|
|
+ sendWithPromiseReturnId(action: string, data: any, priority?: number): { promise: Promise<any>, callbackId: number } {
|
|
|
+ const callbackId = this.getNextId()
|
|
|
+ const message = { action, data, callbackId, priority }
|
|
|
+ const capability = this.createPromiseCapability()
|
|
|
+ this.callbacksCapabilities[callbackId] = capability
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.postMessage(message)
|
|
|
+ } catch (error) {
|
|
|
+ capability.reject(error)
|
|
|
+ }
|
|
|
+
|
|
|
+ return { promise: capability.promise, callbackId }
|
|
|
+ }
|
|
|
+
|
|
|
+ sendWithPromiseWithId(action: string, callbackId: number, data: any): Promise<any> {
|
|
|
+ if (callbackId > this.callbackId) {
|
|
|
+ console.error(`Can't reuse callbackId ${callbackId} lesser than callbackId ${this.callbackId}`)
|
|
|
+ }
|
|
|
+ if (callbackId in this.callbacksCapabilities) {
|
|
|
+ console.error(`Can't reuse callbackId ${callbackId}. There is a capability waiting to be resolved.`)
|
|
|
+ }
|
|
|
+
|
|
|
+ const message = { action, data, callbackId }
|
|
|
+ const capability = this.createPromiseCapability()
|
|
|
+ this.callbacksCapabilities[callbackId] = capability
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.postMessage(message)
|
|
|
+ } catch (error) {
|
|
|
+ capability.reject(error)
|
|
|
+ }
|
|
|
+
|
|
|
+ return capability.promise
|
|
|
+ }
|
|
|
+
|
|
|
+ sendError(error: any, callbackId: number) {
|
|
|
+ if (error.message || error.errorData) {
|
|
|
+ if (error.message && error.message.message) {
|
|
|
+ error.message = error.message.message
|
|
|
+ }
|
|
|
+ const errorData = error.errorData
|
|
|
+ error = {
|
|
|
+ type: error.type ? error.type : "JavascriptError",
|
|
|
+ message: error.message
|
|
|
+ }
|
|
|
+ if (errorData) {
|
|
|
+ Object.keys(errorData).forEach(key => {
|
|
|
+ if (errorData.hasOwnProperty(key)) {
|
|
|
+ error[key] = errorData[key]
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.postMessage({
|
|
|
+ isReply: true,
|
|
|
+ callbackId,
|
|
|
+ error
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ getPromise(callbackId: number): PromiseCapability<any> | undefined {
|
|
|
+ if (callbackId in this.callbacksCapabilities) {
|
|
|
+ return this.callbacksCapabilities[callbackId]
|
|
|
+ }
|
|
|
+ console.error(`Cannot get promise for callback ${callbackId}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ cancelPromise(callbackId: number) {
|
|
|
+ if (callbackId in this.callbacksCapabilities) {
|
|
|
+ const capability = this.callbacksCapabilities[callbackId]
|
|
|
+ delete this.callbacksCapabilities[callbackId]
|
|
|
+ capability.reject({
|
|
|
+ type: "Cancelled",
|
|
|
+ message: "Request has been cancelled."
|
|
|
+ })
|
|
|
+ this.postMessage({
|
|
|
+ action: "actionCancel",
|
|
|
+ data: { callbackId }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ console.warn(`Cannot cancel callback ${callbackId}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ postMessage(message: any) {
|
|
|
+ if (this.postMessageTransfers) {
|
|
|
+ const transfers = this.getTransfersArray(message)
|
|
|
+ this.comObj.postMessage(message, transfers)
|
|
|
+ } else {
|
|
|
+ this.comObj.postMessage(message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getObjectTransfers(obj: any, transfers: any[]) {
|
|
|
+ if (obj !== null && typeof obj === "object") {
|
|
|
+ if (obj instanceof Uint8Array) {
|
|
|
+ transfers.push(obj.buffer)
|
|
|
+ } else if (obj instanceof ArrayBuffer) {
|
|
|
+ transfers.push(obj)
|
|
|
+ } else {
|
|
|
+ Object.keys(obj).forEach(key => {
|
|
|
+ if (obj.hasOwnProperty(key)) {
|
|
|
+ this.getObjectTransfers(obj[key], transfers)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getTransfersArray(message: any): any[] | undefined {
|
|
|
+ const transfers: any[] = []
|
|
|
+ this.getObjectTransfers(message, transfers)
|
|
|
+ return transfers.length === 0 ? undefined : transfers
|
|
|
+ }
|
|
|
+
|
|
|
+ handleMessage(event: MessageEvent) {
|
|
|
+ const data = event.data
|
|
|
+
|
|
|
+ const actionHandler = this.actionHandler
|
|
|
+ const actionHandlerAsync = this.actionHandlerAsync
|
|
|
+ const callbacksCapabilities = this.callbacksCapabilities
|
|
|
+
|
|
|
+ if (data.action in actionHandler) {
|
|
|
+ const handler = actionHandler[data.action]
|
|
|
+ if (data.callbackId) {
|
|
|
+ Promise.resolve()
|
|
|
+ .then(() => handler[0].call(handler[1], data.data))
|
|
|
+ .then(result => {
|
|
|
+ this.postMessage({
|
|
|
+ isReply: true,
|
|
|
+ callbackId: data.callbackId,
|
|
|
+ data: result
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ this.sendError(error, data.callbackId)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ handler[0].call(handler[1], data.data)
|
|
|
+ }
|
|
|
+ } else if (data.action in actionHandlerAsync) {
|
|
|
+ const handler = actionHandlerAsync[data.action]
|
|
|
+ if (data.callbackId) {
|
|
|
+ handler[0].call(handler[1], data)
|
|
|
+ .then(result => {
|
|
|
+ this.postMessage({
|
|
|
+ isReply: true,
|
|
|
+ callbackId: data.callbackId,
|
|
|
+ data: result
|
|
|
+ })
|
|
|
+ this.nextAsync()
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ this.sendError(error, data.callbackId)
|
|
|
+ this.nextAsync()
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ handler[0].call(handler[1], data)
|
|
|
+ .then(() => {
|
|
|
+ this.nextAsync()
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ this.nextAsync()
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.error(`Unknown action from worker: ${data.action}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private createPromiseCapability<T>(): PromiseCapability<T> {
|
|
|
+ const { promise, resolve, reject } = Promise.withResolvers<T>()
|
|
|
+ return { promise, resolve, reject }
|
|
|
+ }
|
|
|
+}
|
|
|
+export default MessageHandler
|