diff --git a/go.mod b/go.mod index 4b7c1a51dfb84d77a09e025955ebeb0f8660cd81..9167d4c0fafbc4e4949c2d386758963694029869 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module gitlab.com/elixxir/xxdk-wasm go 1.19 require ( + github.com/aquilax/truncate v1.0.0 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/hack-pad/go-indexeddb v0.2.0 + github.com/hack-pad/safejs v0.1.1 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.7.0 github.com/spf13/jwalterweatherman v1.1.0 diff --git a/go.sum b/go.sum index 4f4af4fd747eced99afaf9e9eef656edd6d868fa..6bb989eea6333413a48590279bb7b1eb29e4a5fc 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9or github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/aquilax/truncate v1.0.0 h1:UgIGS8U/aZ4JyOJ2h3xcF5cSQ06+gGBnjxH2RUHJe0U= +github.com/aquilax/truncate v1.0.0/go.mod h1:BeMESIDMlvlS3bmg4BVvBbbZUNwWtS8uzYPAKXwwhLw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -165,6 +167,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hack-pad/go-indexeddb v0.2.0 h1:QHDM6gLrtCJvHdHUK8UdibJu4xWQlIDs4+l8L65AUdA= github.com/hack-pad/go-indexeddb v0.2.0/go.mod h1:NH8CaojufPNcKYDhy5JkjfyBXE/72oJPeiywlabN/lM= +github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8= +github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/indexedDb/impl/channels/callbacks.go b/indexedDb/impl/channels/callbacks.go index d6d188a7340263ab9f995ea19f4fb9f7260c14f1..a2ac5e3d9c927207de354679e397f9255f4d9374 100644 --- a/indexedDb/impl/channels/callbacks.go +++ b/indexedDb/impl/channels/callbacks.go @@ -15,12 +15,14 @@ import ( "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/channels" "gitlab.com/elixxir/client/v4/cmix/rounds" cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" "gitlab.com/elixxir/crypto/fastRNG" idbCrypto "gitlab.com/elixxir/crypto/indexedDb" "gitlab.com/elixxir/crypto/message" + "gitlab.com/elixxir/wasm-utils/exception" wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels" "gitlab.com/elixxir/xxdk-wasm/worker" "gitlab.com/xx_network/crypto/csprng" @@ -45,8 +47,8 @@ func (m *manager) registerCallbacks() { m.wtm.RegisterCallback(wChannels.ReceiveMessageTag, m.receiveMessageCB) m.wtm.RegisterCallback(wChannels.ReceiveReplyTag, m.receiveReplyCB) m.wtm.RegisterCallback(wChannels.ReceiveReactionTag, m.receiveReactionCB) - m.wtm.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUUIDCB) - m.wtm.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIDCB) + m.wtm.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUuidCB) + m.wtm.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIdCB) m.wtm.RegisterCallback(wChannels.GetMessageTag, m.getMessageCB) m.wtm.RegisterCallback(wChannels.DeleteMessageTag, m.deleteMessageCB) m.wtm.RegisterCallback(wChannels.MuteUserTag, m.muteUserCB) @@ -54,12 +56,13 @@ func (m *manager) registerCallbacks() { // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty // slice on success or an error message on failure. -func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) { +func (m *manager) newWASMEventModelCB(message []byte, reply func(message []byte)) { var msg wChannels.NewWASMEventModelMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return []byte{}, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + reply([]byte(errors.Wrapf(err, + "failed to JSON unmarshal %T from main thread", msg).Error())) + return } // Create new encryption cipher @@ -67,16 +70,18 @@ func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) { encryption, err := idbCrypto.NewCipherFromJSON( []byte(msg.EncryptionJSON), rng.GetStream()) if err != nil { - return []byte{}, errors.Errorf( - "failed to JSON unmarshal Cipher from main thread: %+v", err) + reply([]byte(errors.Wrap(err, + "failed to JSON unmarshal Cipher from main thread").Error())) + return } m.model, err = NewWASMEventModel(msg.DatabaseName, encryption, m) if err != nil { - return []byte(err.Error()), nil + reply([]byte(err.Error())) + return } - return []byte{}, nil + reply(nil) } // EventUpdate implements [bindings.ChannelUICallbacks.EventUpdate]. @@ -88,49 +93,55 @@ func (m *manager) EventUpdate(eventType int64, jsonData []byte) { } data, err := json.Marshal(msg) if err != nil { - jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err) - return + exception.Throwf("[CH] Could not JSON marshal %T for EventUpdate "+ + "callback: %+v", msg, err) } // Send it to the main thread - m.wtm.SendMessage(wChannels.EventUpdateCallbackTag, data) + err = m.wtm.SendNoResponse(wChannels.EventUpdateCallbackTag, data) + if err != nil { + exception.Throwf( + "[CH] Could not send message for EventUpdate callback: %+v", err) + } } // joinChannelCB is the callback for wasmModel.JoinChannel. Always returns nil; // meaning, no response is supplied (or expected). -func (m *manager) joinChannelCB(data []byte) ([]byte, error) { +func (m *manager) joinChannelCB(message []byte, _ func([]byte)) { var channel cryptoBroadcast.Channel - err := json.Unmarshal(data, &channel) + err := json.Unmarshal(message, &channel) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", channel, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal %T from main thread: "+ + "%+v", channel, err) + return } m.model.JoinChannel(&channel) - return nil, nil } // leaveChannelCB is the callback for wasmModel.LeaveChannel. Always returns // nil; meaning, no response is supplied (or expected). -func (m *manager) leaveChannelCB(data []byte) ([]byte, error) { - channelID, err := id.Unmarshal(data) +func (m *manager) leaveChannelCB(message []byte, _ func([]byte)) { + channelID, err := id.Unmarshal(message) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", channelID, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal %T from main thread: "+ + "%+v", channelID, err) + return } m.model.LeaveChannel(channelID) - return nil, nil } // receiveMessageCB is the callback for wasmModel.ReceiveMessage. Returns a UUID // of 0 on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveMessageCB(data []byte) ([]byte, error) { +func (m *manager) receiveMessageCB(message []byte, reply func(message []byte)) { var msg channels.ModelMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal payload for "+ + "ReceiveMessage from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveMessage(msg.ChannelID, msg.MessageID, msg.Nickname, @@ -138,21 +149,25 @@ func (m *manager) receiveMessageCB(data []byte) ([]byte, error) { msg.Timestamp, msg.Lease, rounds.Round{ID: msg.Round}, msg.Type, msg.Status, msg.Hidden) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[CH] Could not JSON marshal UUID for ReceiveMessage: %+v", err) } - return uuidData, nil + + reply(replyMsg) } // receiveReplyCB is the callback for wasmModel.ReceiveReply. Returns a UUID of // 0 on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveReplyCB(data []byte) ([]byte, error) { +func (m *manager) receiveReplyCB(message []byte, reply func(message []byte)) { var msg wChannels.ReceiveReplyMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal payload for "+ + "ReceiveReply from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveReply(msg.ChannelID, msg.MessageID, msg.ReactionTo, @@ -160,21 +175,25 @@ func (m *manager) receiveReplyCB(data []byte) ([]byte, error) { msg.CodesetVersion, msg.Timestamp, msg.Lease, rounds.Round{ID: msg.Round}, msg.Type, msg.Status, msg.Hidden) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[CH] Could not JSON marshal UUID for ReceiveReply: %+v", err) } - return uuidData, nil + + reply(replyMsg) } // receiveReactionCB is the callback for wasmModel.ReceiveReaction. Returns a // UUID of 0 on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveReactionCB(data []byte) ([]byte, error) { +func (m *manager) receiveReactionCB(message []byte, reply func(message []byte)) { var msg wChannels.ReceiveReplyMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal payload for "+ + "ReceiveReaction from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveReaction(msg.ChannelID, msg.MessageID, @@ -182,22 +201,26 @@ func (m *manager) receiveReactionCB(data []byte) ([]byte, error) { msg.DmToken, msg.CodesetVersion, msg.Timestamp, msg.Lease, rounds.Round{ID: msg.Round}, msg.Type, msg.Status, msg.Hidden) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[CH] Could not JSON marshal UUID for ReceiveReaction: %+v", err) } - return uuidData, nil + + reply(replyMsg) } -// updateFromUUIDCB is the callback for wasmModel.UpdateFromUUID. Always returns +// updateFromUuidCB is the callback for wasmModel.UpdateFromUUID. Always returns // nil; meaning, no response is supplied (or expected). -func (m *manager) updateFromUUIDCB(data []byte) ([]byte, error) { +func (m *manager) updateFromUuidCB(messageData []byte, reply func(message []byte)) { var msg wChannels.MessageUpdateInfo - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(messageData, &msg) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + reply([]byte(errors.Errorf("failed to JSON unmarshal %T from main "+ + "thread: %+v", msg, err).Error())) + return } + var messageID *message.ID var timestamp *time.Time var round *rounds.Round @@ -225,20 +248,31 @@ func (m *manager) updateFromUUIDCB(data []byte) ([]byte, error) { err = m.model.UpdateFromUUID( msg.UUID, messageID, timestamp, round, pinned, hidden, status) if err != nil { - return []byte(err.Error()), nil + reply([]byte(err.Error())) } - return nil, nil + reply(nil) } -// updateFromMessageIDCB is the callback for wasmModel.UpdateFromMessageID. +// updateFromMessageIdCB is the callback for wasmModel.UpdateFromMessageID. // Always returns nil; meaning, no response is supplied (or expected). -func (m *manager) updateFromMessageIDCB(data []byte) ([]byte, error) { +func (m *manager) updateFromMessageIdCB(message []byte, reply func(message []byte)) { + var ue wChannels.UuidError + defer func() { + if replyMessage, err := json.Marshal(ue); err != nil { + exception.Throwf("[CH] Failed to JSON marshal %T for "+ + "UpdateFromMessageID: %+v", ue, err) + } else { + reply(replyMessage) + } + }() + var msg wChannels.MessageUpdateInfo - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + ue.Error = errors.Errorf( + "failed to JSON unmarshal %T from main thread: %+v", msg, err).Error() + return } var timestamp *time.Time var round *rounds.Round @@ -260,84 +294,72 @@ func (m *manager) updateFromMessageIDCB(data []byte) ([]byte, error) { status = &msg.Status } - var ue wChannels.UuidError uuid, err := m.model.UpdateFromMessageID( msg.MessageID, timestamp, round, pinned, hidden, status) if err != nil { - ue.Error = []byte(err.Error()) + ue.Error = err.Error() } else { ue.UUID = uuid } - - data, err = json.Marshal(ue) - if err != nil { - return nil, errors.Errorf("failed to JSON marshal %T: %+v", ue, err) - } - - return data, nil } // getMessageCB is the callback for wasmModel.GetMessage. Returns JSON // marshalled channels.GetMessageMessage. If an error occurs, then Error will // be set with the error message. Otherwise, Message will be set. Only one field // will be set. -func (m *manager) getMessageCB(data []byte) ([]byte, error) { - messageID, err := message.UnmarshalID(data) +func (m *manager) getMessageCB(messageData []byte, reply func(message []byte)) { + var replyMsg wChannels.GetMessageMessage + defer func() { + if replyMessage, err := json.Marshal(replyMsg); err != nil { + exception.Throwf("[CH] Failed to JSON marshal %T for "+ + "GetMessage: %+v", replyMsg, err) + } else { + reply(replyMessage) + } + }() + + messageID, err := message.UnmarshalID(messageData) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", messageID, err) + replyMsg.Error = errors.Errorf("failed to JSON unmarshal %T from "+ + "main thread: %+v", messageID, err).Error() + return } - reply := wChannels.GetMessageMessage{} - msg, err := m.model.GetMessage(messageID) if err != nil { - reply.Error = err.Error() + replyMsg.Error = err.Error() } else { - reply.Message = msg + replyMsg.Message = msg } - - messageData, err := json.Marshal(reply) - if err != nil { - return nil, errors.Errorf("failed to JSON marshal %T from main thread "+ - "for GetMessage reply: %+v", reply, err) - } - return messageData, nil } // deleteMessageCB is the callback for wasmModel.DeleteMessage. Always returns // nil; meaning, no response is supplied (or expected). -func (m *manager) deleteMessageCB(data []byte) ([]byte, error) { - messageID, err := message.UnmarshalID(data) +func (m *manager) deleteMessageCB(messageData []byte, reply func(message []byte)) { + messageID, err := message.UnmarshalID(messageData) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", messageID, err) + reply([]byte(errors.Errorf("failed to JSON unmarshal %T from main "+ + "thread: %+v", messageID, err).Error())) + return } err = m.model.DeleteMessage(messageID) if err != nil { - return []byte(err.Error()), nil + reply([]byte(err.Error())) } - return nil, nil + reply(nil) } // muteUserCB is the callback for wasmModel.MuteUser. Always returns nil; // meaning, no response is supplied (or expected). -func (m *manager) muteUserCB(data []byte) ([]byte, error) { +func (m *manager) muteUserCB(message []byte, _ func([]byte)) { var msg wChannels.MuteUserMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) - } - - channelID := &id.ID{} - err = channelID.UnmarshalJSON(msg.ChannelID) - if err != nil { - return nil, errors.Wrapf(err, "ChannelID: %+v", msg.ChannelID) + jww.ERROR.Printf("[CH] Could not JSON unmarshal %T for MuteUser from "+ + "main thread: %+v", msg, err) + return } - m.model.MuteUser(channelID, msg.PubKey, msg.Unmute) - - return nil, nil + m.model.MuteUser(msg.ChannelID, msg.PubKey, msg.Unmute) } diff --git a/indexedDb/impl/channels/channelsIndexedDbWorker.js b/indexedDb/impl/channels/channelsIndexedDbWorker.js index c109cba89735425f23c0d4d436532a3e3eb9a852..4ca8fa6c5d5910ed80b200efa00f2f2fa7d3073f 100644 --- a/indexedDb/impl/channels/channelsIndexedDbWorker.js +++ b/indexedDb/impl/channels/channelsIndexedDbWorker.js @@ -12,6 +12,10 @@ const isReady = new Promise((resolve) => { }); const go = new Go(); +go.argv = [ + '--logLevel=2', + '--threadLogLevel=2', +] const binPath = 'xxdk-channelsIndexedDkWorker.wasm' WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); diff --git a/indexedDb/impl/channels/main.go b/indexedDb/impl/channels/main.go index b84f13dc205ecd8bfe2466ea9ecb827770f31754..7213825175820be161e1b6e8d0d29efde49c545f 100644 --- a/indexedDb/impl/channels/main.go +++ b/indexedDb/impl/channels/main.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/wasm-utils/exception" "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -51,10 +52,27 @@ var channelsCmd = &cobra.Command{ jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER) jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.") - m := &manager{ - wtm: worker.NewThreadManager("ChannelsIndexedDbWorker", true), + tm, err := worker.NewThreadManager("ChannelsIndexedDbWorker", true) + if err != nil { + exception.ThrowTrace(err) } + m := &manager{wtm: tm} m.registerCallbacks() + + m.wtm.RegisterMessageChannelCallback(worker.LoggerTag, + func(port js.Value, channelName string) { + p := worker.DefaultParams() + p.MessageLogging = false + err = logging.EnableThreadLogging( + logLevel, threadLogLevel, 0, channelName, port) + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } + + jww.INFO.Print("TEST channel") + }) + m.wtm.SignalReady() // Indicate to the Javascript caller that the WASM is ready by resolving @@ -68,13 +86,20 @@ var channelsCmd = &cobra.Command{ } var ( - logLevel jww.Threshold + logLevel jww.Threshold + threadLogLevel jww.Threshold ) func init() { // Initialize all startup flags - channelsCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + channelsCmd.Flags().IntVarP((*int)(&logLevel), + "logLevel", "l", int(jww.LevelDebug), "Sets the log level output when outputting to the Javascript console. "+ "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ "5 = CRITICAL, 6 = FATAL, -1 = disabled.") + channelsCmd.Flags().IntVarP((*int)(&threadLogLevel), + "threadLogLevel", "m", int(jww.LevelDebug), + "The log level when outputting to the worker file buffer. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") } diff --git a/indexedDb/impl/dm/callbacks.go b/indexedDb/impl/dm/callbacks.go index 80d5c3757ed66c326767c434a8725b13658e0197..b7df8739eb90f3ed58b59bd985fc09f48bbbd227 100644 --- a/indexedDb/impl/dm/callbacks.go +++ b/indexedDb/impl/dm/callbacks.go @@ -19,6 +19,7 @@ import ( "gitlab.com/elixxir/client/v4/dm" "gitlab.com/elixxir/crypto/fastRNG" idbCrypto "gitlab.com/elixxir/crypto/indexedDb" + "gitlab.com/elixxir/wasm-utils/exception" wDm "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/dm" "gitlab.com/elixxir/xxdk-wasm/worker" "gitlab.com/xx_network/crypto/csprng" @@ -49,12 +50,13 @@ func (m *manager) registerCallbacks() { // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty // slice on success or an error message on failure. -func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) { +func (m *manager) newWASMEventModelCB(message []byte, reply func(message []byte)) { var msg wDm.NewWASMEventModelMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return []byte{}, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + reply([]byte(errors.Wrapf(err, + "failed to JSON unmarshal %T from main thread", msg).Error())) + return } // Create new encryption cipher @@ -62,17 +64,19 @@ func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) { encryption, err := idbCrypto.NewCipherFromJSON( []byte(msg.EncryptionJSON), rng.GetStream()) if err != nil { - return []byte{}, errors.Errorf("failed to JSON unmarshal channel "+ - "cipher from main thread: %+v", err) + reply([]byte(errors.Wrap(err, + "failed to JSON unmarshal Cipher from main thread").Error())) + return } m.model, err = NewWASMEventModel( msg.DatabaseName, encryption, m.messageReceivedCallback) if err != nil { - return []byte(err.Error()), nil + reply([]byte(err.Error())) + return } - return []byte{}, nil + reply(nil) } // messageReceivedCallback sends calls to the MessageReceivedCallback in the @@ -96,142 +100,169 @@ func (m *manager) messageReceivedCallback(uuid uint64, pubKey ed25519.PublicKey, } // Send it to the main thread - m.wtm.SendMessage(wDm.MessageReceivedCallbackTag, data) + err = m.wtm.SendNoResponse(wDm.MessageReceivedCallbackTag, data) + if err != nil { + exception.Throwf("[DM] Could not send message for "+ + "MessageReceivedCallback: %+v", err) + } } // receiveCB is the callback for wasmModel.Receive. Returns a UUID of 0 on error // or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveCB(data []byte) ([]byte, error) { +func (m *manager) receiveCB(message []byte, reply func(message []byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for Receive "+ + "from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.Receive( - msg.MessageID, msg.Nickname, msg.Text, msg.PartnerKey, msg.SenderKey, msg.DmToken, - msg.Codeset, msg.Timestamp, msg.Round, msg.MType, msg.Status) + msg.MessageID, msg.Nickname, msg.Text, msg.PartnerKey, msg.SenderKey, + msg.DmToken, msg.Codeset, msg.Timestamp, msg.Round, msg.MType, + msg.Status) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[DM] Could not JSON marshal UUID for Receive: %+v", err) } - return uuidData, nil + + reply(replyMsg) } // receiveTextCB is the callback for wasmModel.ReceiveText. Returns a UUID of 0 // on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveTextCB(data []byte) ([]byte, error) { +func (m *manager) receiveTextCB(message []byte, reply func(message []byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return []byte{}, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for "+ + "ReceiveText from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveText( - msg.MessageID, msg.Nickname, string(msg.Text), msg.PartnerKey, msg.SenderKey, msg.DmToken, - msg.Codeset, msg.Timestamp, msg.Round, msg.Status) + msg.MessageID, msg.Nickname, string(msg.Text), msg.PartnerKey, + msg.SenderKey, msg.DmToken, msg.Codeset, msg.Timestamp, msg.Round, + msg.Status) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return []byte{}, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[DM] Could not JSON marshal UUID for ReceiveText: %+v", err) } - return uuidData, nil + reply(replyMsg) } // receiveReplyCB is the callback for wasmModel.ReceiveReply. Returns a UUID of // 0 on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveReplyCB(data []byte) ([]byte, error) { +func (m *manager) receiveReplyCB(message []byte, reply func(message []byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for "+ + "ReceiveReply from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveReply(msg.MessageID, msg.ReactionTo, msg.Nickname, string(msg.Text), msg.PartnerKey, msg.SenderKey, msg.DmToken, msg.Codeset, msg.Timestamp, msg.Round, msg.Status) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[DM] Could not JSON marshal UUID for ReceiveReply: %+v", err) } - return uuidData, nil + reply(replyMsg) } // receiveReactionCB is the callback for wasmModel.ReceiveReaction. Returns a // UUID of 0 on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveReactionCB(data []byte) ([]byte, error) { +func (m *manager) receiveReactionCB(message []byte, reply func(message []byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for "+ + "ReceiveReaction from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveReaction(msg.MessageID, msg.ReactionTo, msg.Nickname, string(msg.Text), msg.PartnerKey, msg.SenderKey, msg.DmToken, msg.Codeset, msg.Timestamp, msg.Round, msg.Status) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[DM] Could not JSON marshal UUID for ReceiveReaction: %+v", err) } - return uuidData, nil + reply(replyMsg) } // updateSentStatusCB is the callback for wasmModel.UpdateSentStatus. Always // returns nil; meaning, no response is supplied (or expected). -func (m *manager) updateSentStatusCB(data []byte) ([]byte, error) { +func (m *manager) updateSentStatusCB(message []byte, _ func([]byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal %T for "+ + "UpdateSentStatus from main thread: %+v", msg, err) + return } m.model.UpdateSentStatus( msg.UUID, msg.MessageID, msg.Timestamp, msg.Round, msg.Status) - - return nil, nil } // deleteMessageCB is the callback for wasmModel.DeleteMessage. Returns a JSON // marshalled bool. -func (m *manager) deleteMessageCB(data []byte) ([]byte, error) { +func (m *manager) deleteMessageCB(message []byte, reply func(message []byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return []byte{}, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal %T for "+ + "UpdateSentStatus from main thread: %+v", msg, err) + reply([]byte{0}) + return } - deleted := m.model.DeleteMessage(msg.MessageID, msg.SenderKey) - - boolData, err := json.Marshal(deleted) - if err != nil { - return []byte{}, errors.Errorf("failed to JSON marshal bool: %+v", err) + if m.model.DeleteMessage(msg.MessageID, msg.SenderKey) { + reply([]byte{1}) + return } - - return boolData, nil + reply([]byte{0}) } // getConversationCB is the callback for wasmModel.GetConversation. // Returns nil on error or the JSON marshalled Conversation on success. -func (m *manager) getConversationCB(data []byte) ([]byte, error) { - result := m.model.GetConversation(data) - return json.Marshal(result) +func (m *manager) getConversationCB(message []byte, reply func(message []byte)) { + result := m.model.GetConversation(message) + replyMessage, err := json.Marshal(result) + if err != nil { + exception.Throwf("[DM] Could not JSON marshal %T for "+ + "GetConversation: %+v", result, err) + } + reply(replyMessage) } // getConversationsCB is the callback for wasmModel.GetConversations. // Returns nil on error or the JSON marshalled list of Conversation on success. -func (m *manager) getConversationsCB(_ []byte) ([]byte, error) { +func (m *manager) getConversationsCB(_ []byte, reply func(message []byte)) { result := m.model.GetConversations() - return json.Marshal(result) + replyMessage, err := json.Marshal(result) + if err != nil { + exception.Throwf("[DM] Could not JSON marshal %T for "+ + "GetConversations: %+v", result, err) + } + reply(replyMessage) } diff --git a/indexedDb/impl/dm/dmIndexedDbWorker.js b/indexedDb/impl/dm/dmIndexedDbWorker.js index 8a5fdbf8ad9a02967b408985a0219647003eaf7e..41a5492fb799fe8c27db59252c18d6d8ad454c90 100644 --- a/indexedDb/impl/dm/dmIndexedDbWorker.js +++ b/indexedDb/impl/dm/dmIndexedDbWorker.js @@ -12,6 +12,10 @@ const isReady = new Promise((resolve) => { }); const go = new Go(); +go.argv = [ + '--logLevel=2', + '--threadLogLevel=2', +] const binPath = 'xxdk-dmIndexedDkWorker.wasm' WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); diff --git a/indexedDb/impl/dm/main.go b/indexedDb/impl/dm/main.go index 96fae8e6fbdbce3767739891d4d7ea466e08149c..0034e9265d86a79c76a2b604ae725e846688f09f 100644 --- a/indexedDb/impl/dm/main.go +++ b/indexedDb/impl/dm/main.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/wasm-utils/exception" "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -52,10 +53,27 @@ var dmCmd = &cobra.Command{ jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER) jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.") - m := &manager{ - wtm: worker.NewThreadManager("DmIndexedDbWorker", true), + tm, err := worker.NewThreadManager("DmIndexedDbWorker", true) + if err != nil { + exception.ThrowTrace(err) } + m := &manager{wtm: tm} m.registerCallbacks() + + m.wtm.RegisterMessageChannelCallback(worker.LoggerTag, + func(port js.Value, channelName string) { + p := worker.DefaultParams() + p.MessageLogging = false + err = logging.EnableThreadLogging( + logLevel, threadLogLevel, 0, channelName, port) + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } + + jww.INFO.Print("TEST channel") + }) + m.wtm.SignalReady() // Indicate to the Javascript caller that the WASM is ready by resolving @@ -69,13 +87,20 @@ var dmCmd = &cobra.Command{ } var ( - logLevel jww.Threshold + logLevel jww.Threshold + threadLogLevel jww.Threshold ) func init() { // Initialize all startup flags - dmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + dmCmd.Flags().IntVarP((*int)(&logLevel), + "logLevel", "l", int(jww.LevelDebug), "Sets the log level output when outputting to the Javascript console. "+ "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ "5 = CRITICAL, 6 = FATAL, -1 = disabled.") + dmCmd.Flags().IntVarP((*int)(&threadLogLevel), + "threadLogLevel", "m", int(jww.LevelDebug), + "The log level when outputting to the worker file buffer. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") } diff --git a/indexedDb/impl/state/callbacks.go b/indexedDb/impl/state/callbacks.go index 73dbc2a50ec2e45237803259a50066e239acc0ef..5bbfcc7d89a3dd8675c47824725c3add02b38f8e 100644 --- a/indexedDb/impl/state/callbacks.go +++ b/indexedDb/impl/state/callbacks.go @@ -12,8 +12,9 @@ package main import ( "encoding/json" "github.com/pkg/errors" - "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" stateWorker "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/state" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -35,39 +36,48 @@ func (m *manager) registerCallbacks() { // newStateCB is the callback for NewState. Returns an empty // slice on success or an error message on failure. -func (m *manager) newStateCB(data []byte) ([]byte, error) { +func (m *manager) newStateCB(message []byte, reply func(message []byte)) { var msg stateWorker.NewStateMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return []byte{}, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + reply([]byte(errors.Wrapf(err, + "failed to JSON unmarshal %T from main thread", msg).Error())) + return } m.model, err = NewState(msg.DatabaseName) if err != nil { - return []byte(err.Error()), nil + reply([]byte(err.Error())) + return } - return []byte{}, nil + reply(nil) } // setCB is the callback for stateModel.Set. // Returns nil on error or the resulting byte data on success. -func (m *manager) setCB(data []byte) ([]byte, error) { +func (m *manager) setCB(message []byte, reply func(message []byte)) { var msg stateWorker.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) + if err != nil { + reply([]byte(errors.Wrapf(err, + "failed to JSON unmarshal %T from main thread", msg).Error())) + return + } + + err = m.model.Set(msg.Key, msg.Value) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + reply([]byte(err.Error())) + return } - return nil, m.model.Set(msg.Key, msg.Value) + reply(nil) } // getCB is the callback for stateModel.Get. // Returns nil on error or the resulting byte data on success. -func (m *manager) getCB(data []byte) ([]byte, error) { - key := string(data) +func (m *manager) getCB(message []byte, reply func(message []byte)) { + key := string(message) result, err := m.model.Get(key) msg := stateWorker.TransferMessage{ Key: key, @@ -75,5 +85,10 @@ func (m *manager) getCB(data []byte) ([]byte, error) { Error: err.Error(), } - return json.Marshal(msg) + replyMessage, err := json.Marshal(msg) + if err != nil { + exception.Throwf("Could not JSON marshal %T for Get: %+v", msg, err) + } + + reply(replyMessage) } diff --git a/indexedDb/impl/state/main.go b/indexedDb/impl/state/main.go index 719b9969852c2ba946d6365367050878b0dd7b81..2344486a2c3b1c6ab55db13110d4d4305b16ed0f 100644 --- a/indexedDb/impl/state/main.go +++ b/indexedDb/impl/state/main.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/wasm-utils/exception" "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -27,16 +28,16 @@ const SEMVER = "0.1.0" func main() { // Set to os.Args because the default is os.Args[1:] and in WASM, args start // at 0, not 1. - channelsCmd.SetArgs(os.Args) + stateCmd.SetArgs(os.Args) - err := channelsCmd.Execute() + err := stateCmd.Execute() if err != nil { fmt.Println(err) os.Exit(1) } } -var channelsCmd = &cobra.Command{ +var stateCmd = &cobra.Command{ Use: "stateIndexedDbWorker", Short: "IndexedDb database for state.", Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]", @@ -51,10 +52,27 @@ var channelsCmd = &cobra.Command{ jww.INFO.Printf("xxDK state web worker version: v%s", SEMVER) jww.INFO.Print("[WW] Starting xxDK WebAssembly State Database Worker.") - m := &manager{ - wtm: worker.NewThreadManager("StateIndexedDbWorker", true), + tm, err := worker.NewThreadManager("DmIndexedDbWorker", true) + if err != nil { + exception.ThrowTrace(err) } + m := &manager{wtm: tm} m.registerCallbacks() + + m.wtm.RegisterMessageChannelCallback(worker.LoggerTag, + func(port js.Value, channelName string) { + p := worker.DefaultParams() + p.MessageLogging = false + err = logging.EnableThreadLogging( + logLevel, threadLogLevel, 0, channelName, port) + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } + + jww.INFO.Print("TEST channel") + }) + m.wtm.SignalReady() // Indicate to the Javascript caller that the WASM is ready by resolving @@ -68,13 +86,20 @@ var channelsCmd = &cobra.Command{ } var ( - logLevel jww.Threshold + logLevel jww.Threshold + threadLogLevel jww.Threshold ) func init() { // Initialize all startup flags - channelsCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + stateCmd.Flags().IntVarP((*int)(&logLevel), + "logLevel", "l", int(jww.LevelDebug), "Sets the log level output when outputting to the Javascript console. "+ "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ "5 = CRITICAL, 6 = FATAL, -1 = disabled.") + stateCmd.Flags().IntVarP((*int)(&threadLogLevel), + "threadLogLevel", "m", int(jww.LevelDebug), + "The log level when outputting to the worker file buffer. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") } diff --git a/indexedDb/impl/state/stateIndexedDbWorker.js b/indexedDb/impl/state/stateIndexedDbWorker.js index a7c440d2a862a81737d75418ef39e5464c99ad90..cff3e34af865755070e6ff634465b1b82b12ced3 100644 --- a/indexedDb/impl/state/stateIndexedDbWorker.js +++ b/indexedDb/impl/state/stateIndexedDbWorker.js @@ -12,6 +12,10 @@ const isReady = new Promise((resolve) => { }); const go = new Go(); +go.argv = [ + '--logLevel=2', + '--threadLogLevel=2', +] const binPath = 'xxdk-stateIndexedDkWorker.wasm' WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); diff --git a/indexedDb/worker/channels/implementation.go b/indexedDb/worker/channels/implementation.go index 2e11702432dd5d4b1dae668715ccb6a7bdc50950..d6495de4fae213db99481992e38306a49f2bcd4d 100644 --- a/indexedDb/worker/channels/implementation.go +++ b/indexedDb/worker/channels/implementation.go @@ -41,12 +41,17 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { return } - w.wm.SendMessage(JoinChannelTag, data, nil) + if err = w.wm.SendNoResponse(JoinChannelTag, data); err != nil { + jww.FATAL.Panicf("[CH] Failed to send to %q: %+v", JoinChannelTag, err) + } } // LeaveChannel is called whenever a channel is left locally. func (w *wasmModel) LeaveChannel(channelID *id.ID) { - w.wm.SendMessage(LeaveChannelTag, channelID.Marshal(), nil) + err := w.wm.SendNoResponse(LeaveChannelTag, channelID.Marshal()) + if err != nil { + jww.FATAL.Panicf("[CH] Failed to send to %q: %+v", LeaveChannelTag, err) + } } // ReceiveMessage is called whenever a message is received on a given channel. @@ -81,27 +86,20 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID, return 0 } - uuidChan := make(chan uint64) - w.wm.SendMessage(ReceiveMessageTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+ - "ReceiveMessage: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) + response, err := w.wm.SendMessage(ReceiveMessageTag, data) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", ReceiveMessageTag, err) + } - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+ - "the worker about ReceiveMessage", worker.ResponseTimeout) + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[CH] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveMessageTag, err) + return 0 } - return 0 + return uuid } // ReceiveReplyMessage is JSON marshalled and sent to the worker for @@ -149,27 +147,20 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID, return 0 } - uuidChan := make(chan uint64) - w.wm.SendMessage(ReceiveReplyTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+ - "ReceiveReply: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) + response, err := w.wm.SendMessage(ReceiveReplyTag, data) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", ReceiveReplyTag, err) + } - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+ - "the worker about ReceiveReply", worker.ResponseTimeout) + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[CH] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveReplyTag, err) + return 0 } - return 0 + return uuid } // ReceiveReaction is called whenever a reaction to a message is received on a @@ -211,27 +202,20 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID, return 0 } - uuidChan := make(chan uint64) - w.wm.SendMessage(ReceiveReactionTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+ - "ReceiveReaction: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) + response, err := w.wm.SendMessage(ReceiveReactionTag, data) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", ReceiveReactionTag, err) + } - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+ - "the worker about ReceiveReply", worker.ResponseTimeout) + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[CH] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveReactionTag, err) + return 0 } - return 0 + return uuid } // MessageUpdateInfo is JSON marshalled and sent to the worker for @@ -301,29 +285,22 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, "could not JSON marshal payload for UpdateFromUUID: %+v", err) } - errChan := make(chan error) - w.wm.SendMessage(UpdateFromUUIDTag, data, func(data []byte) { - if data != nil { - errChan <- errors.New(string(data)) - } else { - errChan <- nil - } - }) - - select { - case err = <-errChan: - return err - case <-time.After(worker.ResponseTimeout): - return errors.Errorf("timed out after %s waiting for response from "+ - "the worker about UpdateFromUUID", worker.ResponseTimeout) + response, err := w.wm.SendMessage(UpdateFromUUIDTag, data) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", UpdateFromUUIDTag, err) + } else if len(response) > 0 { + return errors.New(string(response)) } + + return nil } // UuidError is JSON marshalled and sent to the worker for // [wasmModel.UpdateFromMessageID]. type UuidError struct { UUID uint64 `json:"uuid"` - Error []byte `json:"error"` + Error string `json:"error"` } // UpdateFromMessageID is called whenever a message with the message ID is @@ -367,29 +344,20 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID, "UpdateFromMessageID: %+v", err) } - uuidChan := make(chan uint64) - errChan := make(chan error) - w.wm.SendMessage(UpdateFromMessageIDTag, data, - func(data []byte) { - var ue UuidError - if err = json.Unmarshal(data, &ue); err != nil { - errChan <- errors.Errorf("could not JSON unmarshal response "+ - "to UpdateFromMessageID: %+v", err) - } else if ue.Error != nil { - errChan <- errors.New(string(ue.Error)) - } else { - uuidChan <- ue.UUID - } - }) - - select { - case uuid := <-uuidChan: - return uuid, nil - case err = <-errChan: - return 0, err - case <-time.After(worker.ResponseTimeout): - return 0, errors.Errorf("timed out after %s waiting for response from "+ - "the worker about UpdateFromMessageID", worker.ResponseTimeout) + response, err := w.wm.SendMessage(UpdateFromMessageIDTag, data) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", UpdateFromMessageIDTag, err) + } + + var ue UuidError + if err = json.Unmarshal(response, &ue); err != nil { + return 0, errors.Errorf("could not JSON unmarshal response to %q: %+v", + UpdateFromMessageIDTag, err) + } else if len(ue.Error) > 0 { + return 0, errors.New(ue.Error) + } else { + return ue.UUID, nil } } @@ -403,65 +371,52 @@ type GetMessageMessage struct { // GetMessage returns the message with the given [channel.MessageID]. func (w *wasmModel) GetMessage( messageID message.ID) (channels.ModelMessage, error) { - msgChan := make(chan GetMessageMessage) - w.wm.SendMessage(GetMessageTag, messageID.Marshal(), - func(data []byte) { - var msg GetMessageMessage - err := json.Unmarshal(data, &msg) - if err != nil { - jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+ - "GetMessage: %+v", err) - } - msgChan <- msg - }) - - select { - case msg := <-msgChan: - if msg.Error != "" { - return channels.ModelMessage{}, errors.New(msg.Error) - } - return msg.Message, nil - case <-time.After(worker.ResponseTimeout): - return channels.ModelMessage{}, errors.Errorf("timed out after %s "+ - "waiting for response from the worker about GetMessage", - worker.ResponseTimeout) + + response, err := w.wm.SendMessage(GetMessageTag, messageID.Marshal()) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", GetMessageTag, err) + } + + var msg GetMessageMessage + if err = json.Unmarshal(response, &msg); err != nil { + return channels.ModelMessage{}, errors.Wrapf(err, + "[CH] Could not JSON unmarshal response to %q", GetMessageTag) } + + if msg.Error != "" { + return channels.ModelMessage{}, errors.New(msg.Error) + } + + return msg.Message, nil } // DeleteMessage removes a message with the given messageID from storage. func (w *wasmModel) DeleteMessage(messageID message.ID) error { - errChan := make(chan error) - w.wm.SendMessage(DeleteMessageTag, messageID.Marshal(), - func(data []byte) { - if data != nil { - errChan <- errors.New(string(data)) - } else { - errChan <- nil - } - }) - - select { - case err := <-errChan: - return err - case <-time.After(worker.ResponseTimeout): - return errors.Errorf("timed out after %s waiting for response from "+ - "the worker about DeleteMessage", worker.ResponseTimeout) + response, err := w.wm.SendMessage(DeleteMessageTag, messageID.Marshal()) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", DeleteMessageTag, err) + } else if len(response) > 0 { + return errors.New(string(response)) } + + return nil } // MuteUserMessage is JSON marshalled and sent to the worker for // [wasmModel.MuteUser]. type MuteUserMessage struct { - ChannelID []byte `json:"channelID"` - PubKey []byte `json:"pubKey"` - Unmute bool `json:"unmute"` + ChannelID *id.ID `json:"channelID"` + PubKey ed25519.PublicKey `json:"pubKey"` + Unmute bool `json:"unmute"` } // MuteUser is called whenever a user is muted or unmuted. func (w *wasmModel) MuteUser( channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) { msg := MuteUserMessage{ - ChannelID: channelID.Marshal(), + ChannelID: channelID, PubKey: pubKey, Unmute: unmute, } @@ -472,5 +427,8 @@ func (w *wasmModel) MuteUser( return } - w.wm.SendMessage(MuteUserTag, data, nil) + err = w.wm.SendNoResponse(MuteUserTag, data) + if err != nil { + jww.FATAL.Panicf("[CH] Failed to send to %q: %+v", MuteUserTag, err) + } } diff --git a/indexedDb/worker/channels/init.go b/indexedDb/worker/channels/init.go index d14a80ebfc5d91dd3e100e58d7ec2cb45f7d281e..56c82e74bdef65b71a0305500eb74d839df1b6b7 100644 --- a/indexedDb/worker/channels/init.go +++ b/indexedDb/worker/channels/init.go @@ -11,7 +11,6 @@ package channels import ( "encoding/json" - "time" "github.com/pkg/errors" @@ -19,6 +18,7 @@ import ( "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/client/v4/channels" idbCrypto "gitlab.com/elixxir/crypto/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -27,7 +27,7 @@ import ( const databaseSuffix = "_speakeasy" // eventUpdateCallback is the [bindings.ChannelUICallback] callback function -// it has a type ([bindings.NickNameUpdate] to [bindings.MessageDeleted] +// it has a type [bindings.NickNameUpdate] to [bindings.MessageDeleted] // and json data that is the callback information. type eventUpdateCallback func(eventType int64, jsonData []byte) @@ -67,6 +67,15 @@ func NewWASMEventModel(path, wasmJsPath string, encryption idbCrypto.Cipher, wm.RegisterCallback(EventUpdateCallbackTag, messageReceivedCallbackHandler(channelCbs.EventUpdate)) + // Create MessageChannel between worker and logger so that the worker logs + // are saved + err = worker.CreateMessageChannel(logging.GetLogger().Worker(), wm, + "channelsIndexedDbLogger", worker.LoggerTag) + if err != nil { + return nil, errors.Wrap(err, "Failed to create message channel "+ + "between channel indexedDb worker and logger") + } + // Store the database name err = storage.StoreIndexedDb(databaseName) if err != nil { @@ -95,18 +104,12 @@ func NewWASMEventModel(path, wasmJsPath string, encryption idbCrypto.Cipher, return nil, err } - dataChan := make(chan []byte) - wm.SendMessage(NewWASMEventModelTag, payload, - func(data []byte) { dataChan <- data }) - - select { - case data := <-dataChan: - if len(data) > 0 { - return nil, errors.New(string(data)) - } - case <-time.After(worker.ResponseTimeout): - return nil, errors.Errorf("timed out after %s waiting for indexedDB "+ - "database in worker to initialize", worker.ResponseTimeout) + response, err := wm.SendMessage(NewWASMEventModelTag, payload) + if err != nil { + return nil, errors.Wrapf(err, + "failed to send message %q", NewWASMEventModelTag) + } else if len(response) > 0 { + return nil, errors.New(string(response)) } return &wasmModel{wm}, nil @@ -121,10 +124,10 @@ type EventUpdateCallbackMessage struct { // messageReceivedCallbackHandler returns a handler to manage messages for the // MessageReceivedCallback. -func messageReceivedCallbackHandler(cb eventUpdateCallback) func(data []byte) { - return func(data []byte) { +func messageReceivedCallbackHandler(cb eventUpdateCallback) worker.ReceiverCallback { + return func(message []byte, _ func([]byte)) { var msg EventUpdateCallbackMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { jww.ERROR.Printf( "Failed to JSON unmarshal %T from worker: %+v", msg, err) diff --git a/indexedDb/worker/dm/implementation.go b/indexedDb/worker/dm/implementation.go index 2764613b518e97cb57306b0956ad7497f7f59e63..ed021462504dd35816c20839ee0cfbd61babef54 100644 --- a/indexedDb/worker/dm/implementation.go +++ b/indexedDb/worker/dm/implementation.go @@ -64,31 +64,23 @@ func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte, data, err := json.Marshal(msg) if err != nil { jww.ERROR.Printf( - "Could not JSON marshal payload for TransferMessage: %+v", err) + "[DM] Could not JSON marshal payload for Receive: %+v", err) return 0 } - uuidChan := make(chan uint64) - w.wh.SendMessage(ReceiveTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to Receive: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about Receive", worker.ResponseTimeout) + response, err := w.wh.SendMessage(ReceiveTag, data) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveTag, err) } - return 0 + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveTag, err) + return 0 + } + + return uuid } func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string, @@ -114,27 +106,19 @@ func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string, return 0 } - uuidChan := make(chan uint64) - w.wh.SendMessage(ReceiveTextTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveText: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveText", worker.ResponseTimeout) + response, err := w.wh.SendMessage(ReceiveTextTag, data) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveTextTag, err) + } + + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveTextTag, err) + return 0 } - return 0 + return uuid } func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname, @@ -161,27 +145,19 @@ func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname, return 0 } - uuidChan := make(chan uint64) - w.wh.SendMessage(ReceiveReplyTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveReply: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveReply", worker.ResponseTimeout) + response, err := w.wh.SendMessage(ReceiveReplyTag, data) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveReplyTag, err) } - return 0 + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveReplyTag, err) + return 0 + } + + return uuid } func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname, @@ -208,27 +184,19 @@ func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname, return 0 } - uuidChan := make(chan uint64) - w.wh.SendMessage(ReceiveReactionTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveReaction: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveReaction", worker.ResponseTimeout) + response, err := w.wh.SendMessage(ReceiveReactionTag, data) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveReactionTag, err) + } + + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveReactionTag, err) + return 0 } - return 0 + return uuid } func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID, @@ -247,10 +215,13 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID, "Could not JSON marshal payload for TransferMessage: %+v", err) } - w.wh.SendMessage(UpdateSentStatusTag, data, nil) + if err = w.wh.SendNoResponse(UpdateSentStatusTag, data); err != nil { + jww.FATAL.Panicf("[CH] Failed to send to %q: %+v", UpdateSentStatusTag, err) + } } -func (w *wasmModel) DeleteMessage(messageID message.ID, senderPubKey ed25519.PublicKey) bool { +func (w *wasmModel) DeleteMessage( + messageID message.ID, senderPubKey ed25519.PublicKey) bool { msg := TransferMessage{ MessageID: messageID, SenderKey: senderPubKey, @@ -262,71 +233,45 @@ func (w *wasmModel) DeleteMessage(messageID message.ID, senderPubKey ed25519.Pub "Could not JSON marshal payload for TransferMessage: %+v", err) } - resultChan := make(chan bool) - w.wh.SendMessage(DeleteMessageTag, data, - func(data []byte) { - var result bool - if len(data) > 0 { - if err = json.Unmarshal(data, &result); err != nil { - jww.ERROR.Printf("Could not JSON unmarshal response to "+ - "DeleteMessage: %+v", err) - } - } - resultChan <- result - }) - - select { - case result := <-resultChan: - return result - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about DeleteMessage", worker.ResponseTimeout) - return false + response, err := w.wh.SendMessage(DeleteMessageTag, data) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", DeleteMessageTag, err) + } else if len(response) == 0 { + jww.FATAL.Panicf( + "[DM] Received empty response from %q", DeleteMessageTag) } + + return response[0] == 1 } func (w *wasmModel) GetConversation(senderPubKey ed25519.PublicKey) *dm.ModelConversation { - resultChan := make(chan *dm.ModelConversation) - w.wh.SendMessage(GetConversationTag, senderPubKey, - func(data []byte) { - var result *dm.ModelConversation - err := json.Unmarshal(data, &result) - if err != nil { - jww.ERROR.Printf("Could not JSON unmarshal response to "+ - "GetConversation: %+v", err) - } - resultChan <- result - }) - - select { - case result := <-resultChan: - return result - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about GetConversation", worker.ResponseTimeout) + response, err := w.wh.SendMessage(GetConversationTag, senderPubKey) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", GetConversationTag, err) + } + + var result dm.ModelConversation + if err = json.Unmarshal(response, &result); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal %T from worker for "+ + "%q: %+v", result, GetConversationTag, err) return nil } + + return &result } func (w *wasmModel) GetConversations() []dm.ModelConversation { - resultChan := make(chan []dm.ModelConversation) - w.wh.SendMessage(GetConversationTag, nil, - func(data []byte) { - var result []dm.ModelConversation - err := json.Unmarshal(data, &result) - if err != nil { - jww.ERROR.Printf("Could not JSON unmarshal response to "+ - "GetConversations: %+v", err) - } - resultChan <- result - }) - - select { - case result := <-resultChan: - return result - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about GetConversations", worker.ResponseTimeout) + response, err := w.wh.SendMessage(GetConversationsTag, nil) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", GetConversationsTag, err) + } + + var result []dm.ModelConversation + if err = json.Unmarshal(response, &result); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal %T from worker for "+ + "%q: %+v", result, GetConversationsTag, err) return nil } + + return result } diff --git a/indexedDb/worker/dm/init.go b/indexedDb/worker/dm/init.go index b7afc2b2237c2258feb91084e644402e775c45f1..0c934488bd8719de7ff7d303aa14686560475787 100644 --- a/indexedDb/worker/dm/init.go +++ b/indexedDb/worker/dm/init.go @@ -12,13 +12,13 @@ package dm import ( "crypto/ed25519" "encoding/json" - "time" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/dm" idbCrypto "gitlab.com/elixxir/crypto/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -55,6 +55,15 @@ func NewWASMEventModel(path, wasmJsPath string, encryption idbCrypto.Cipher, wh.RegisterCallback( MessageReceivedCallbackTag, messageReceivedCallbackHandler(cb)) + // Create MessageChannel between worker and logger so that the worker logs + // are saved + err = worker.CreateMessageChannel(logging.GetLogger().Worker(), wh, + "dmIndexedDbLogger", worker.LoggerTag) + if err != nil { + return nil, errors.Wrap(err, "Failed to create message channel "+ + "between DM indexedDb worker and logger") + } + // Store the database name err = storage.StoreIndexedDb(databaseName) if err != nil { @@ -83,18 +92,12 @@ func NewWASMEventModel(path, wasmJsPath string, encryption idbCrypto.Cipher, return nil, err } - dataChan := make(chan []byte) - wh.SendMessage(NewWASMEventModelTag, payload, - func(data []byte) { dataChan <- data }) - - select { - case data := <-dataChan: - if len(data) > 0 { - return nil, errors.New(string(data)) - } - case <-time.After(worker.ResponseTimeout): - return nil, errors.Errorf("timed out after %s waiting for indexedDB "+ - "database in worker to initialize", worker.ResponseTimeout) + response, err := wh.SendMessage(NewWASMEventModelTag, payload) + if err != nil { + return nil, errors.Wrapf(err, + "failed to send message %q", NewWASMEventModelTag) + } else if len(response) > 0 { + return nil, errors.New(string(response)) } return &wasmModel{wh}, nil @@ -105,19 +108,19 @@ func NewWASMEventModel(path, wasmJsPath string, encryption idbCrypto.Cipher, type MessageReceivedCallbackMessage struct { UUID uint64 `json:"uuid"` PubKey ed25519.PublicKey `json:"pubKey"` - MessageUpdate bool `json:"message_update"` - ConversationUpdate bool `json:"conversation_update"` + MessageUpdate bool `json:"messageUpdate"` + ConversationUpdate bool `json:"conversationUpdate"` } // messageReceivedCallbackHandler returns a handler to manage messages for the // MessageReceivedCallback. -func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte) { - return func(data []byte) { +func messageReceivedCallbackHandler(cb MessageReceivedCallback) worker.ReceiverCallback { + return func(message []byte, _ func([]byte)) { var msg MessageReceivedCallbackMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - jww.ERROR.Printf("Failed to JSON unmarshal "+ - "MessageReceivedCallback message from worker: %+v", err) + jww.ERROR.Printf("[DM] Failed to JSON unmarshal %T message from "+ + "worker: %+v", msg, err) return } cb(msg.UUID, msg.PubKey, msg.MessageUpdate, msg.ConversationUpdate) diff --git a/indexedDb/worker/state/implementation.go b/indexedDb/worker/state/implementation.go index 92a5752ade4aef34f002a3904cc586f047e4c6bb..17eb1228a873a54131f6a48dedbcd2afc4c312a0 100644 --- a/indexedDb/worker/state/implementation.go +++ b/indexedDb/worker/state/implementation.go @@ -11,8 +11,9 @@ package dm import ( "encoding/json" + "github.com/pkg/errors" - "time" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -40,43 +41,32 @@ func (w *wasmModel) Set(key string, value []byte) error { "Could not JSON marshal payload for TransferMessage: %+v", err) } - resultChan := make(chan []byte) - w.wh.SendMessage(SetTag, data, - func(data []byte) { - resultChan <- data - }) - - select { - case result := <-resultChan: - return errors.New(string(result)) - case <-time.After(worker.ResponseTimeout): - return errors.Errorf("Timed out after %s waiting for response from the "+ - "worker about Get", worker.ResponseTimeout) + response, err := w.wh.SendMessage(SetTag, data) + if err != nil { + jww.FATAL.Panicf("Failed to send message to %q: %+v", SetTag, err) + } else if len(response) > 0 { + return errors.New(string(response)) } + + return nil } func (w *wasmModel) Get(key string) ([]byte, error) { - resultChan := make(chan []byte) - w.wh.SendMessage(GetTag, []byte(key), - func(data []byte) { - resultChan <- data - }) - - select { - case result := <-resultChan: - var msg TransferMessage - err := json.Unmarshal(result, &msg) - if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) - } - - if len(msg.Error) > 0 { - return nil, errors.New(msg.Error) - } - return msg.Value, nil - case <-time.After(worker.ResponseTimeout): - return nil, errors.Errorf("Timed out after %s waiting for response from the "+ - "worker about Get", worker.ResponseTimeout) + + response, err := w.wh.SendMessage(GetTag, []byte(key)) + if err != nil { + jww.FATAL.Panicf("Failed to send message to %q: %+v", GetTag, err) } + + var msg TransferMessage + if err = json.Unmarshal(response, &msg); err != nil { + return nil, errors.Errorf( + "failed to JSON unmarshal %T from worker: %+v", msg, err) + } + + if len(msg.Error) > 0 { + return nil, errors.New(msg.Error) + } + + return msg.Value, nil } diff --git a/indexedDb/worker/state/init.go b/indexedDb/worker/state/init.go index 3ed7ac513204b26912f17a298d320b089d1fbf4e..4477924c77f3a35e27be1178cb15cfa0957455d6 100644 --- a/indexedDb/worker/state/init.go +++ b/indexedDb/worker/state/init.go @@ -11,11 +11,12 @@ package dm import ( "encoding/json" - "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" - "time" "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" + "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -46,6 +47,15 @@ func NewState(path, wasmJsPath string) (impl.WebState, error) { return nil, err } + // Create MessageChannel between worker and logger so that the worker logs + // are saved + err = worker.CreateMessageChannel(logging.GetLogger().Worker(), wh, + "stateIndexedDbLogger", worker.LoggerTag) + if err != nil { + return nil, errors.Wrap(err, "Failed to create message channel "+ + "between state indexedDb worker and logger") + } + // Store the database name err = storage.StoreIndexedDb(databaseName) if err != nil { @@ -61,18 +71,11 @@ func NewState(path, wasmJsPath string) (impl.WebState, error) { return nil, err } - dataChan := make(chan []byte) - wh.SendMessage(NewStateTag, payload, - func(data []byte) { dataChan <- data }) - - select { - case data := <-dataChan: - if len(data) > 0 { - return nil, errors.New(string(data)) - } - case <-time.After(worker.ResponseTimeout): - return nil, errors.Errorf("timed out after %s waiting for indexedDB "+ - "database in worker to initialize", worker.ResponseTimeout) + response, err := wh.SendMessage(NewStateTag, payload) + if err != nil { + jww.FATAL.Panicf("Failed to send message to %q: %+v", NewStateTag, err) + } else if len(response) > 0 { + return nil, errors.New(string(response)) } return &wasmModel{wh}, nil diff --git a/logging/console.go b/logging/console.go index 8c2edd821210d338f73ff18bfabedfc2f319d7d9..bfc249d1f3284ef856c2692060284c5493a0d900 100644 --- a/logging/console.go +++ b/logging/console.go @@ -10,9 +10,10 @@ package logging import ( - jww "github.com/spf13/jwalterweatherman" "io" "syscall/js" + + jww "github.com/spf13/jwalterweatherman" ) var consoleObj = js.Global().Get("console") diff --git a/logging/fileLogger_test.go b/logging/fileLogger_test.go index 317f69544a927280827aa4f3739c7f1e39d30ac4..c28e89cc7a799d0750548e9b84353d21fc551ff4 100644 --- a/logging/fileLogger_test.go +++ b/logging/fileLogger_test.go @@ -11,11 +11,12 @@ package logging import ( "bytes" - "github.com/armon/circbuf" - jww "github.com/spf13/jwalterweatherman" "math/rand" "reflect" "testing" + + "github.com/armon/circbuf" + jww "github.com/spf13/jwalterweatherman" ) func Test_newFileLogger(t *testing.T) { diff --git a/logging/jwwListeners.go b/logging/jwwListeners.go deleted file mode 100644 index 7d83f3d5bcc69190c505dfd0d0676fbe6104d1d0..0000000000000000000000000000000000000000 --- a/logging/jwwListeners.go +++ /dev/null @@ -1,78 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -package logging - -import ( - jww "github.com/spf13/jwalterweatherman" - "sync" -) - -// logListeners contains a map of all registered log listeners keyed on a unique -// ID that can be used to remove the listener once it has been added. This -// global keeps track of all listeners that are registered to jwalterweatherman -// logging. -var logListeners = newLogListenerList() - -type logListenerList struct { - listeners map[uint64]jww.LogListener - currentID uint64 - sync.Mutex -} - -func newLogListenerList() logListenerList { - return logListenerList{ - listeners: make(map[uint64]jww.LogListener), - currentID: 0, - } -} - -// AddLogListener registers the log listener with jwalterweatherman. Returns a -// unique ID that can be used to remove the listener. -func AddLogListener(ll jww.LogListener) uint64 { - logListeners.Lock() - defer logListeners.Unlock() - - id := logListeners.addLogListener(ll) - jww.SetLogListeners(logListeners.mapToSlice()...) - return id -} - -// RemoveLogListener unregisters the log listener with the ID from -// jwalterweatherman. -func RemoveLogListener(id uint64) { - logListeners.Lock() - defer logListeners.Unlock() - - logListeners.removeLogListener(id) - jww.SetLogListeners(logListeners.mapToSlice()...) - -} - -// addLogListener adds the listener to the list and returns its unique ID. -func (lll *logListenerList) addLogListener(ll jww.LogListener) uint64 { - id := lll.currentID - lll.currentID++ - lll.listeners[id] = ll - - return id -} - -// removeLogListener removes the listener with the specified ID from the list. -func (lll *logListenerList) removeLogListener(id uint64) { - delete(lll.listeners, id) -} - -// mapToSlice converts the map of listeners to a slice of listeners so that it -// can be registered with jwalterweatherman.SetLogListeners. -func (lll *logListenerList) mapToSlice() []jww.LogListener { - listeners := make([]jww.LogListener, 0, len(lll.listeners)) - for _, l := range lll.listeners { - listeners = append(listeners, l) - } - return listeners -} diff --git a/logging/logger.go b/logging/logger.go index 8155210daf6b0b81667334d8675f63bd6b2aaca5..81197d22f98ec09c640f9d1b5fbdd42b9181f926 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -26,6 +26,7 @@ const ( WriteLogTag worker.Tag = "WriteLog" GetFileTag worker.Tag = "GetFile" GetFileExtTag worker.Tag = "GetFileExt" + MaxSizeTag worker.Tag = "MaxSize" SizeTag worker.Tag = "Size" ) @@ -38,6 +39,7 @@ func GetLogger() Logger { return logger } +// Logger controls and accesses the log file for this binary. type Logger interface { // StopLogging stops log message writes. Once logging is stopped, it cannot // be resumed and the log file cannot be recovered. @@ -65,6 +67,21 @@ type Logger interface { // worker file buffer. This must be called only once at initialisation. func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int, workerScriptURL, workerName string) error { + return enableLogging(logLevel, fileLogLevel, maxLogFileSizeMB, + workerScriptURL, workerName, js.Undefined()) +} + +// EnableThreadLogging enables logging to the Javascript console and to +// a local or remote thread file buffer. This must be called only once at +// initialisation. +func EnableThreadLogging(logLevel, fileLogLevel jww.Threshold, + maxLogFileSizeMB int, workerName string, messagePort js.Value) error { + return enableLogging( + logLevel, fileLogLevel, maxLogFileSizeMB, "", workerName, messagePort) +} + +func enableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int, + workerScriptURL, workerName string, messagePort js.Value) error { var listeners []jww.LogListener if logLevel > -1 { @@ -80,13 +97,7 @@ func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int, if fileLogLevel > -1 { maxLogFileSize := maxLogFileSizeMB * 1_000_000 - if workerScriptURL == "" { - fl, err := newFileLogger(fileLogLevel, maxLogFileSize) - if err != nil { - return errors.Wrap(err, "could not initialize logging to file") - } - listeners = append(listeners, fl.Listen) - } else { + if workerScriptURL != "" { wl, err := newWorkerLogger( fileLogLevel, maxLogFileSize, workerScriptURL, workerName) if err != nil { @@ -94,6 +105,18 @@ func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int, } listeners = append(listeners, wl.Listen) + } else if !messagePort.IsUndefined() { + tl, err := newThreadLogger(fileLogLevel, workerName, messagePort) + if err != nil { + return errors.Wrap(err, "could not initialize logging on message port") + } + listeners = append(listeners, tl.Listen) + } else { + fl, err := newFileLogger(fileLogLevel, maxLogFileSize) + if err != nil { + return errors.Wrap(err, "could not initialize logging to file") + } + listeners = append(listeners, fl.Listen) } js.Global().Set("GetLogger", js.FuncOf(GetLoggerJS)) @@ -121,6 +144,7 @@ func GetLoggerJS(js.Value, []js.Value) any { return newLoggerJS(LoggerJS{GetLogger()}) } +// LoggerJS is the Javascript wrapper for the Logger. type LoggerJS struct { api Logger } diff --git a/logging/workerLogger.go b/logging/workerLogger.go index bfac4703943c94956a8dc6501b722e7d0e5cad62..41f95f7490a292fa459ecb4e6476592280bdc376 100644 --- a/logging/workerLogger.go +++ b/logging/workerLogger.go @@ -14,7 +14,6 @@ import ( "encoding/json" "io" "math" - "time" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -22,8 +21,6 @@ import ( "gitlab.com/elixxir/xxdk-wasm/worker" ) -// TODO: add ability to import worker so that multiple threads can send logs: https://stackoverflow.com/questions/8343781/how-to-do-worker-to-worker-communication - // workerLogger manages the recording of jwalterweatherman logs to the in-memory // file buffer in a remote Worker thread. type workerLogger struct { @@ -52,7 +49,7 @@ func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int, // Register the callback used by the Javascript to request the log file. // This prevents an error print when GetFileExtTag is not registered. - wl.wm.RegisterCallback(GetFileExtTag, func([]byte) { + wl.wm.RegisterCallback(GetFileExtTag, func([]byte, func([]byte)) { jww.DEBUG.Print("[LOG] Received file requested from external " + "Javascript. Ignoring file.") }) @@ -63,24 +60,12 @@ func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int, } // Send message to initialize the log file listener - errChan := make(chan error) - wl.wm.SendMessage(NewLogFileTag, data, func(data []byte) { - if len(data) > 0 { - errChan <- errors.New(string(data)) - } else { - errChan <- nil - } - }) - - // Wait for worker to respond - select { - case err = <-errChan: - if err != nil { - return nil, err - } - case <-time.After(worker.ResponseTimeout): - return nil, errors.Errorf("timed out after %s waiting for new log "+ - "file in worker to initialize", worker.ResponseTimeout) + response, err := wl.wm.SendMessage(NewLogFileTag, data) + if err != nil { + return nil, errors.Wrap(err, "failed to initialize the log file listener") + } else if response != nil { + return nil, errors.Wrap(errors.New(string(response)), + "failed to initialize the log file listener") } jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level "+ @@ -91,11 +76,9 @@ func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int, } // Write adheres to the io.Writer interface and sends the log entries to the -// worker to be added to the file buffer. Always returns the length of p and -// nil. All errors are printed to the log. +// worker to be added to the file buffer. Always returns the length of p. func (wl *workerLogger) Write(p []byte) (n int, err error) { - wl.wm.SendMessage(WriteLogTag, p, nil) - return len(p), nil + return len(p), wl.wm.SendNoResponse(WriteLogTag, p) } // Listen adheres to the [jwalterweatherman.LogListener] type and returns the @@ -112,23 +95,22 @@ func (wl *workerLogger) Listen(threshold jww.Threshold) io.Writer { func (wl *workerLogger) StopLogging() { wl.threshold = math.MaxInt - wl.wm.Stop() - jww.DEBUG.Printf("[LOG] Terminated log worker.") + err := wl.wm.Stop() + if err != nil { + jww.ERROR.Printf("[LOG] Failed to terminate log worker: %+v", err) + } else { + jww.DEBUG.Printf("[LOG] Terminated log worker.") + } } // GetFile returns the entire log file. func (wl *workerLogger) GetFile() []byte { - fileChan := make(chan []byte) - wl.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data }) - - select { - case file := <-fileChan: - return file - case <-time.After(worker.ResponseTimeout): - jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ - "file from worker", worker.ResponseTimeout) - return nil + response, err := wl.wm.SendMessage(GetFileTag, nil) + if err != nil { + jww.FATAL.Panicf("[LOG] Failed to get log file from worker: %+v", err) } + + return response } // Threshold returns the log level threshold used in the file. @@ -143,17 +125,12 @@ func (wl *workerLogger) MaxSize() int { // Size returns the number of bytes written to the log file. func (wl *workerLogger) Size() int { - sizeChan := make(chan []byte) - wl.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data }) - - select { - case data := <-sizeChan: - return int(binary.LittleEndian.Uint64(data)) - case <-time.After(worker.ResponseTimeout): - jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ - "file size from worker", worker.ResponseTimeout) - return 0 + response, err := wl.wm.SendMessage(SizeTag, nil) + if err != nil { + jww.FATAL.Panicf("[LOG] Failed to get log size from worker: %+v", err) } + + return int(binary.LittleEndian.Uint64(response)) } // Worker returns the manager for the Javascript Worker object. diff --git a/logging/workerThread/main.go b/logging/workerThread/main.go index 1a9a31a0ca39ee684b4427eef03fc89f4a407b8c..0c969963651354d5776c98b48ce1149814595953 100644 --- a/logging/workerThread/main.go +++ b/logging/workerThread/main.go @@ -17,7 +17,7 @@ import ( "syscall/js" "github.com/armon/circbuf" - "github.com/pkg/errors" + "github.com/hack-pad/safejs" "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" @@ -31,8 +31,8 @@ const SEMVER = "0.1.0" // workerLogFile manages communication with the main thread and writing incoming // logging messages to the log file. type workerLogFile struct { - wtm *worker.ThreadManager - b *circbuf.Buffer + tm *worker.ThreadManager + b *circbuf.Buffer } func main() { @@ -64,11 +64,15 @@ var LoggerCmd = &cobra.Command{ jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.") - wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)} + tm, err := worker.NewThreadManager("Logger", true) + if err != nil { + jww.FATAL.Panicf("Failed to get new thread manager: %+v", err) + } + wlf := workerLogFile{tm: tm} wlf.registerCallbacks() - wlf.wtm.SignalReady() + wlf.tm.SignalReady() // Indicate to the Javascript caller that the WASM is ready by resolving // a promise created by the caller. @@ -96,54 +100,96 @@ func init() { // to get the file and file metadata. func (wlf *workerLogFile) registerCallbacks() { // Callback for logging.LogToFileWorker - wlf.wtm.RegisterCallback(logging.NewLogFileTag, - func(data []byte) ([]byte, error) { + wlf.tm.RegisterCallback(logging.NewLogFileTag, + func(message []byte, reply func([]byte)) { + var maxLogFileSize int64 - err := json.Unmarshal(data, &maxLogFileSize) + err := json.Unmarshal(message, &maxLogFileSize) if err != nil { - return []byte(err.Error()), err + reply([]byte(err.Error())) + return } wlf.b, err = circbuf.NewBuffer(maxLogFileSize) if err != nil { - return []byte(err.Error()), err + reply([]byte(err.Error())) + return } jww.DEBUG.Printf("[LOG] Created new worker log file of size %d", maxLogFileSize) - return []byte{}, nil + reply(nil) }) // Callback for Logging.GetFile - wlf.wtm.RegisterCallback(logging.WriteLogTag, - func(data []byte) ([]byte, error) { - n, err := wlf.b.Write(data) + wlf.tm.RegisterCallback(logging.WriteLogTag, + func(message []byte, _ func([]byte)) { + n, err := wlf.b.Write(message) if err != nil { - return nil, err - } else if n != len(data) { - return nil, errors.Errorf( - "wrote %d bytes; expected %d bytes", n, len(data)) + jww.ERROR.Printf("[LOG] Failed to write to log: %+v", err) + } else if n != len(message) { + jww.ERROR.Printf("[LOG] Failed to write to log: wrote %d "+ + "bytes; expected %d bytes", n, len(message)) } - - return nil, nil }, ) // Callback for Logging.GetFile - wlf.wtm.RegisterCallback(logging.GetFileTag, func([]byte) ([]byte, error) { - return wlf.b.Bytes(), nil + wlf.tm.RegisterCallback(logging.GetFileTag, + func(_ []byte, reply func([]byte)) { reply(wlf.b.Bytes()) }) + + // Callback for Logging.GetFile + wlf.tm.RegisterCallback(logging.GetFileExtTag, + func(_ []byte, reply func([]byte)) { reply(wlf.b.Bytes()) }) + + // Callback for Logging.Size + wlf.tm.RegisterCallback(logging.SizeTag, func(_ []byte, reply func([]byte)) { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(wlf.b.TotalWritten())) + reply(b) }) + wlf.tm.RegisterMessageChannelCallback(worker.LoggerTag, wlf.registerLogWorker) +} + +func (wlf *workerLogFile) registerLogWorker(port js.Value, channelName string) { + p := worker.DefaultParams() + p.MessageLogging = false + mm, err := worker.NewMessageManager( + safejs.Safe(port), channelName+"-logger", p) + if err != nil { + jww.FATAL.Panic(err) + } + + mm.RegisterCallback(logging.WriteLogTag, + func(message []byte, _ func([]byte)) { + n, err := wlf.b.Write(message) + if err != nil { + jww.ERROR.Printf("[LOG] Failed to write to log: %+v", err) + } else if n != len(message) { + jww.ERROR.Printf("[LOG] Failed to write to log: wrote %d "+ + "bytes; expected %d bytes", n, len(message)) + } + }, + ) + // Callback for Logging.GetFile - wlf.wtm.RegisterCallback(logging.GetFileExtTag, func([]byte) ([]byte, error) { - return wlf.b.Bytes(), nil + mm.RegisterCallback(logging.GetFileTag, func(_ []byte, reply func([]byte)) { + reply(wlf.b.Bytes()) + }) + + // Callback for Logging.MaxSize + mm.RegisterCallback(logging.GetFileTag, func(_ []byte, reply func([]byte)) { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(wlf.b.Size())) + reply(b) }) // Callback for Logging.Size - wlf.wtm.RegisterCallback(logging.SizeTag, func([]byte) ([]byte, error) { + mm.RegisterCallback(logging.SizeTag, func(_ []byte, reply func([]byte)) { b := make([]byte, 8) binary.LittleEndian.PutUint64(b, uint64(wlf.b.TotalWritten())) - return b, nil + reply(b) }) } diff --git a/logging/workerThreadLogger.go b/logging/workerThreadLogger.go new file mode 100644 index 0000000000000000000000000000000000000000..900e82de69e4c3a3397c485c197cb9b093824d69 --- /dev/null +++ b/logging/workerThreadLogger.go @@ -0,0 +1,117 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package logging + +import ( + "encoding/binary" + "io" + "math" + "syscall/js" + + "github.com/hack-pad/safejs" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/xxdk-wasm/worker" +) + +// threadLogger manages the recording of jwalterweatherman logs from a worker to +// the in-memory file buffer in a remote Worker thread. +type threadLogger struct { + threshold jww.Threshold + mm *worker.MessageManager +} + +// newThreadLogger starts logging to an in-memory log file in a remote Worker +// at the specified threshold. Returns a [threadLogger] that can be used to get +// the log file. +func newThreadLogger(threshold jww.Threshold, channelName string, + messagePort js.Value) (*threadLogger, error) { + p := worker.DefaultParams() + p.MessageLogging = false + mm, err := worker.NewMessageManager(safejs.Safe(messagePort), + channelName+"-worker", p) + if err != nil { + return nil, err + } + + tl := &threadLogger{ + threshold: threshold, + mm: mm, + } + + jww.FEEDBACK.Printf("[LOG] Worker outputting log to file at level "+ + "%s using web worker", tl.threshold) + + logger = tl + return tl, nil +} + +// Write adheres to the io.Writer interface and sends the log entries to the +// worker to be added to the file buffer. Always returns the length of p and +// nil. All errors are printed to the log. +func (tl *threadLogger) Write(p []byte) (n int, err error) { + return len(p), tl.mm.SendNoResponse(WriteLogTag, p) +} + +// Listen adheres to the [jwalterweatherman.LogListener] type and returns the +// log writer when the threshold is within the set threshold limit. +func (tl *threadLogger) Listen(threshold jww.Threshold) io.Writer { + if threshold < tl.threshold { + return nil + } + return tl +} + +// StopLogging stops sending log messages to the logging worker. Once logging is +// stopped, it cannot be resumed and the log file cannot be recovered. This does +// not stop the logging worker. +func (tl *threadLogger) StopLogging() { + tl.threshold = math.MaxInt +} + +// GetFile returns the entire log file. +func (tl *threadLogger) GetFile() []byte { + response, err := tl.mm.Send(GetFileTag, nil) + if err != nil { + jww.FATAL.Panicf("[LOG] Failed to get log file from worker: %+v", err) + } + + return response +} + +// Threshold returns the log level threshold of logs sent to the worker. +func (tl *threadLogger) Threshold() jww.Threshold { + return tl.threshold +} + +// MaxSize returns the max size, in bytes, that the log file is allowed to be. +func (tl *threadLogger) MaxSize() int { + response, err := tl.mm.Send(MaxSizeTag, nil) + if err != nil { + jww.FATAL.Panicf("[LOG] Failed to max file size from worker: %+v", err) + } + + return int(binary.LittleEndian.Uint64(response)) +} + +// Size returns the number of bytes written to the log file. +func (tl *threadLogger) Size() int { + response, err := tl.mm.Send(SizeTag, nil) + if err != nil { + jww.FATAL.Panicf("[LOG] Failed to file size from worker: %+v", err) + } + + return int(binary.LittleEndian.Uint64(response)) +} + +// Worker always returns nil +func (tl *threadLogger) Worker() *worker.Manager { + return nil +} diff --git a/worker/README.md b/worker/README.md index 18d21f12fb5115a92941451c2f9aa0667ca3fa9a..7d5a4c3b5e3ceb5e1784dc5b119d4ea82213ad15 100644 --- a/worker/README.md +++ b/worker/README.md @@ -18,12 +18,12 @@ package main import ( "fmt" - "gitlab.com/elixxir/wasm-utils/utils/worker" + "gitlab.com/elixxir/xxdk-wasm/worker" ) func main() { fmt.Println("Starting WebAssembly Worker.") - tm := worker.NewThreadManager("exampleWebWorker") + tm := worker.NewThreadManager("exampleWebWorker", true) tm.SignalReady() <-make(chan bool) } @@ -47,8 +47,8 @@ To start the worker, call `worker.NewManager` with the Javascript file to launch the worker. ```go -wm, err := worker.NewManager("workerWasm.js", "exampleWebWorker") +m, err := worker.NewManager("workerWasm.js", "exampleWebWorker", true) if err != nil { - return nil, err +return nil, err } -``` +``` \ No newline at end of file diff --git a/worker/manager.go b/worker/manager.go index 2809a40922609923f300593a3120082afc46f5a5..ca371a576e67f24e648c70d43b7fe1bffc6ad8c2 100644 --- a/worker/manager.go +++ b/worker/manager.go @@ -10,15 +10,11 @@ package worker import ( - "encoding/json" - "sync" "syscall/js" "time" + "github.com/hack-pad/safejs" "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - - "gitlab.com/elixxir/wasm-utils/utils" ) // initID is the ID for the first item in the callback list. If the list only @@ -31,81 +27,43 @@ const ( // workerInitialConnectionTimeout is the time to wait to receive initial // contact from a new worker before timing out. workerInitialConnectionTimeout = 90 * time.Second - - // ResponseTimeout is the general time to wait after sending a message to - // receive a response before timing out. - ResponseTimeout = 30 * time.Second ) -// receiveQueueChanSize is the size of the channel that received messages are -// put on. -const receiveQueueChanSize = 100 - -// ReceptionCallback is called with a message received from the worker. -type ReceptionCallback func(data []byte) - // Manager manages the handling of messages received from the worker. type Manager struct { - // worker is the Worker Javascript object. - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker - worker js.Value - - // callbacks are a list of ReceptionCallback that handle a specific message - // received from the worker. Each callback is keyed on a tag specifying how - // the received message should be handled. If the message is a reply to a - // message sent to the worker, then the callback is also keyed on a unique - // ID. If the message is not a reply, then it appears on initID. - callbacks map[Tag]map[uint64]ReceptionCallback - - // responseIDs is a list of the newest ID to assign to each callback when - // registered. The IDs are used to connect a reply from the worker to the - // original message sent by the main thread. - responseIDs map[Tag]uint64 - - // receiveQueue is the channel that all received messages are queued on - // while they wait to be processed. - receiveQueue chan js.Value - - // quit, when triggered, stops the thread that processes received messages. - quit chan struct{} - - // name describes the worker. It is used for debugging and logging purposes. - name string + mm *MessageManager - // messageLogging determines if debug message logs should be printed every - // time a message is sent/received to/from the worker. - messageLogging bool - - mux sync.Mutex + // Wrapper of the Worker Javascript object. + // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker + w Worker } // NewManager generates a new Manager. This functions will only return once // communication with the worker has been established. func NewManager(aURL, name string, messageLogging bool) (*Manager, error) { - // Create new worker options with the given name - opts := newWorkerOptions("", "", name) - - m := &Manager{ - worker: js.Global().Get("Worker").New(aURL, opts), - callbacks: make(map[Tag]map[uint64]ReceptionCallback), - responseIDs: make(map[Tag]uint64), - receiveQueue: make(chan js.Value, receiveQueueChanSize), - quit: make(chan struct{}), - name: name, - messageLogging: messageLogging, + w, err := NewWorker(aURL, newWorkerOptions("", "", name)) + if err != nil { + return nil, errors.Wrapf(err, "failed to construct Worker") } - // Start thread to process responses from worker - go m.processThread() + p := DefaultParams() + p.MessageLogging = messageLogging + mm, err := NewMessageManager(w.Value, name+"-main", p) + if err != nil { + return nil, errors.Wrapf(err, "failed to construct message manager") + } - // Register listeners on the Javascript worker object that receive messages - // and errors from the worker - m.addEventListeners() + m := &Manager{ + mm: mm, + w: w, + } // Register a callback that will receive initial message from worker // indicating that it is ready ready := make(chan struct{}) - m.RegisterCallback(readyTag, func([]byte) { ready <- struct{}{} }) + mm.RegisterCallback(readyTag, func([]byte, func([]byte)) { + ready <- struct{}{} + }) // Wait for the ready signal from the worker select { @@ -113,263 +71,124 @@ func NewManager(aURL, name string, messageLogging bool) (*Manager, error) { case <-time.After(workerInitialConnectionTimeout): return nil, errors.Errorf("[WW] [%s] timed out after %s waiting for "+ "initial message from worker", - m.name, workerInitialConnectionTimeout) + mm.name, workerInitialConnectionTimeout) } return m, nil } -// Stop closes the worker manager and terminates the worker. -func (m *Manager) Stop() { - // Stop processThread - select { - case m.quit <- struct{}{}: - } - - // Terminate the worker - go m.terminate() -} - -// processThread processes received messages sequentially. -func (m *Manager) processThread() { - jww.INFO.Printf("[WW] [%s] Starting process thread.", m.name) - for { - select { - case <-m.quit: - jww.INFO.Printf("[WW] [%s] Quitting process thread.", m.name) - return - case msgData := <-m.receiveQueue: - - switch msgData.Type() { - case js.TypeObject: - if msgData.Get("constructor").Equal(utils.Uint8Array) { - err := m.processReceivedMessage(utils.CopyBytesToGo(msgData)) - if err != nil { - jww.ERROR.Printf("[WW] [%s] Failed to process received "+ - "message from worker: %+v", m.name, err) - } - break - } - fallthrough - - default: - jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+ - "from worker: %s", m.name, msgData.Type(), - utils.JsToJson(msgData)) - } - } - } -} - -// SendMessage sends a message to the worker with the given tag. If a reception -// callback is specified, then the message is given a unique ID to handle the -// reply. Set receptionCB to nil if no reply is expected. -func (m *Manager) SendMessage( - tag Tag, data []byte, receptionCB ReceptionCallback) { - var id uint64 - if receptionCB != nil { - id = m.registerReplyCallback(tag, receptionCB) - } - - if m.messageLogging { - jww.DEBUG.Printf("[WW] [%s] Main sending message for %q and ID %d "+ - "with data: %s", m.name, tag, id, data) - } +// NewManagerFromScript generates a new Manager. This functions will only return +// once communication with the worker has been established. +// TODO: test or remove +func NewManagerFromScript( + jsScript, name string, messageLogging bool) (*Manager, error) { - msg := Message{ - Tag: tag, - ID: id, - Data: data, - } - payload, err := json.Marshal(msg) + blob, err := jsBlob.New([]any{jsScript}, map[string]any{ + "type": "text/javascript", + }) if err != nil { - jww.FATAL.Panicf("[WW] [%s] Main failed to marshal %T for %q and "+ - "ID %d going to worker: %+v", m.name, msg, tag, id, err) + return nil, err } - - go m.postMessage(payload) -} - -// receiveMessage is registered with the Javascript event listener and is called -// every time a new message from the worker is received. -func (m *Manager) receiveMessage(data js.Value) { - m.receiveQueue <- data -} - -// processReceivedMessage processes the message received from the worker and -// calls the associated callback. This functions blocks until the callback -// returns. -func (m *Manager) processReceivedMessage(data []byte) error { - var msg Message - err := json.Unmarshal(data, &msg) + objectURL, err := jsURL.Call("createObjectURL", blob) if err != nil { - return err - } - - if m.messageLogging { - jww.DEBUG.Printf("[WW] [%s] Main received message for %q and ID %d "+ - "with data: %s", m.name, msg.Tag, msg.ID, msg.Data) + return nil, err } - - callback, err := m.getCallback(msg.Tag, msg.ID, msg.DeleteCB) + objectURLStr, err := objectURL.String() if err != nil { - return err + return nil, err } - callback(msg.Data) - - return nil + return NewManager(objectURLStr, name, messageLogging) } -// getCallback returns the callback for the given ID or returns an error if no -// callback is found. The callback is deleted from the map if specified in the -// message. This function is thread safe. -func (m *Manager) getCallback( - tag Tag, id uint64, deleteCB bool) (ReceptionCallback, error) { - m.mux.Lock() - defer m.mux.Unlock() - callbacks, exists := m.callbacks[tag] - if !exists { - return nil, errors.Errorf("no callbacks found for tag %q", tag) - } - - callback, exists := callbacks[id] - if !exists { - return nil, errors.Errorf("no %q callback found for ID %d", tag, id) - } - - if deleteCB { - delete(m.callbacks[tag], id) - if len(m.callbacks[tag]) == 0 { - delete(m.callbacks, tag) - } - } +// Stop closes the worker manager and terminates the worker. +func (m *Manager) Stop() error { + m.mm.Stop() - return callback, nil + // Terminate the worker + err := m.w.Terminate() + return errors.Wrapf(err, "failed to terminate worker %q", m.mm.name) } -// RegisterCallback registers the reception callback for the given tag. If a -// previous callback was registered, it is overwritten. This function is thread -// safe. -func (m *Manager) RegisterCallback(tag Tag, receptionCB ReceptionCallback) { - m.mux.Lock() - defer m.mux.Unlock() - - id := initID - - jww.DEBUG.Printf("[WW] [%s] Main registering callback for tag %q and ID %d", - m.name, tag, id) - - m.callbacks[tag] = map[uint64]ReceptionCallback{id: receptionCB} +// SendMessage sends a message to the worker with the given tag and waits for a +// response. An error is returned on failure to send or on timeout. +func (m *Manager) SendMessage(tag Tag, data []byte) (response []byte, err error) { + return m.mm.Send(tag, data) } -// RegisterCallback registers the reception callback for the given tag and a new -// unique ID used to associate the reply to the callback. Returns the ID that -// was registered. If a previous callback was registered, it is overwritten. -// This function is thread safe. -func (m *Manager) registerReplyCallback( - tag Tag, receptionCB ReceptionCallback) uint64 { - m.mux.Lock() - defer m.mux.Unlock() - id := m.getNextID(tag) - - jww.DEBUG.Printf("[WW] [%s] Main registering callback for tag %q and ID %d", - m.name, tag, id) - - if _, exists := m.callbacks[tag]; !exists { - m.callbacks[tag] = make(map[uint64]ReceptionCallback) - } - m.callbacks[tag][id] = receptionCB - - return id +// SendTimeout sends a message to the worker with the given tag and waits for a +// response. An error is returned on failure to send or on the specified +// timeout. +func (m *Manager) SendTimeout( + tag Tag, data []byte, timeout time.Duration) (response []byte, err error) { + return m.mm.SendTimeout(tag, data, timeout) } -// getNextID returns the next unique ID for the given tag. This function is not -// thread-safe. -func (m *Manager) getNextID(tag Tag) uint64 { - if _, exists := m.responseIDs[tag]; !exists { - m.responseIDs[tag] = initID - } +// SendNoResponse sends a message to the worker with the given tag. It returns +// immediately and does not wait for a response. +func (m *Manager) SendNoResponse(tag Tag, data []byte) error { + return m.mm.SendNoResponse(tag, data) +} - id := m.responseIDs[tag] - m.responseIDs[tag]++ - return id +// RegisterCallback registers the callback for the given tag. Previous tags are +// overwritten. This function is thread safe. +func (m *Manager) RegisterCallback(tag Tag, receiverCB ReceiverCallback) { + m.mm.RegisterCallback(tag, receiverCB) } -// GetWorker returns the web worker object. This returned so the worker object -// can be returned to the Javascript layer for it to communicate with the worker -// thread. -func (m *Manager) GetWorker() js.Value { return m.worker } +// GetWorker returns the Worker wrapper for the Worker Javascript object. This +// is returned so the worker object can be returned to the Javascript layer for +// it to communicate with the worker thread. +func (m *Manager) GetWorker() js.Value { return safejs.Unsafe(m.w.Value) } // Name returns the name of the web worker object. -func (m *Manager) Name() string { return m.name } +func (m *Manager) Name() string { return m.mm.name } //////////////////////////////////////////////////////////////////////////////// -// Javascript Call Wrappers // +// Worker Wrapper // //////////////////////////////////////////////////////////////////////////////// -// addEventListeners adds the event listeners needed to receive a message from -// the worker. Two listeners were added; one to receive messages from the worker -// and the other to receive errors. -func (m *Manager) addEventListeners() { - // Create a listener for when the message event is fired on the worker. This - // occurs when a message is received from the worker. - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event - messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { - m.receiveMessage(args[0].Get("data")) - return nil - }) - - // Create listener for when an error event is fired on the worker. This - // occurs when an error occurs in the worker. - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event - errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { - event := args[0] - jww.FATAL.Panicf("[WW] [%s] Main received error event: %+v", - m.name, js.Error{Value: event}) - return nil - }) - - // Create listener for when a messageerror event is fired on the worker. - // This occurs when it receives a message that cannot be deserialized. - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event - messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { - event := args[0] - jww.ERROR.Printf("[WW] [%s] Main received message error event: %+v", - m.name, js.Error{Value: event}) - return nil - }) - - // Register each event listener on the worker using addEventListener - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener - m.worker.Call("addEventListener", "message", messageEvent) - m.worker.Call("addEventListener", "error", errorEvent) - m.worker.Call("addEventListener", "messageerror", messageerrorEvent) +// Worker wraps a Javascript Worker object. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker +type Worker struct { + MessagePort } -// postMessage sends a message to the worker. -// -// msg is the object to deliver to the worker; this will be in the data -// field in the event delivered to the worker. It must be a transferable object -// because this function transfers ownership of the message instead of copying -// it for better performance. See the doc for more information. +var ( + jsWorker = safejs.MustGetGlobal("Worker") + jsMessageChannel = safejs.MustGetGlobal("MessageChannel") + jsURL = safejs.MustGetGlobal("URL") + jsBlob = safejs.MustGetGlobal("Blob") +) + +// NewWorker creates a Javascript Worker object that executes the script at the +// specified URL. // -// If the message parameter is not provided, a SyntaxError will be thrown by the -// parser. If the data to be passed to the worker is unimportant, js.Null or -// js.Undefined can be passed explicitly. +// It returns any thrown exceptions as errors. // -// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage -func (m *Manager) postMessage(msg []byte) { - buffer := utils.CopyBytesToJS(msg) - m.worker.Call("postMessage", buffer, []any{buffer.Get("buffer")}) +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker +func NewWorker(aURL string, options map[string]any) (w Worker, err error) { + v, err := jsWorker.New(aURL, options) + if err != nil { + return Worker{}, err + } + + mp, err := NewMessagePort(v) + if err != nil { + return Worker{}, err + } + + return Worker{MessagePort: mp}, nil } -// terminate immediately terminates the Worker. This does not offer the worker +// Terminate immediately terminates the Worker. This does not offer the worker // an opportunity to finish its operations; it is stopped at once. // // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate -func (m *Manager) terminate() { - m.worker.Call("terminate") +func (w Worker) Terminate() error { + _, err := w.Call("terminate") + return err } // newWorkerOptions creates a new Javascript object containing optional @@ -377,8 +196,8 @@ func (m *Manager) terminate() { // // Each property is optional; leave a property empty to use the defaults (as // documented). The available properties are: -// - type - The type of worker to create. The value can be either "classic" or -// "module". If not specified, the default used is "classic". +// - workerType - The type of worker to create. The value can be either +// "classic" or "module". If not specified, the default used is "classic". // - credentials - The type of credentials to use for the worker. The value // can be "omit", "same-origin", or "include". If it is not specified, or if // the type is "classic", then the default used is "omit" (no credentials @@ -387,7 +206,7 @@ func (m *Manager) terminate() { // purposes. // // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker#options -func newWorkerOptions(workerType, credentials, name string) js.Value { +func newWorkerOptions(workerType, credentials, name string) map[string]any { options := make(map[string]any, 3) if workerType != "" { options["type"] = workerType @@ -398,5 +217,5 @@ func newWorkerOptions(workerType, credentials, name string) js.Value { if name != "" { options["name"] = name } - return js.ValueOf(options) + return options } diff --git a/worker/manager_test.go b/worker/manager_test.go index b071c7e437d149eb49aec41da593f858c4c01354..67f5320d87fb977942a922f15e6e6ff4147fd09e 100644 --- a/worker/manager_test.go +++ b/worker/manager_test.go @@ -10,201 +10,39 @@ package worker import ( - "encoding/json" - "reflect" + "syscall/js" "testing" - "time" ) -// Tests Manager.processReceivedMessage calls the expected callback. -func TestManager_processReceivedMessage(t *testing.T) { - m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)} - - msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}, 1) - cb := func([]byte) { cbChan <- struct{}{} } - m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb} - - data, err := json.Marshal(msg) - if err != nil { - t.Fatalf("Failed to JSON marshal Message: %+v", err) - } - - go func() { - err = m.processReceivedMessage(data) - if err != nil { - t.Errorf("Failed to receive message: %+v", err) - } - }() - - select { - case <-cbChan: - case <-time.After(10 * time.Millisecond): - t.Error("Timed out waiting for callback to be called.") - } -} - -// Tests Manager.getCallback returns the expected callback and deletes only the -// given callback when deleteCB is true. -func TestManager_getCallback(t *testing.T) { - m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)} - - // Add new callback and check that it is returned by getCallback - tag, id1 := readyTag, uint64(5) - cb := func([]byte) {} - m.callbacks[tag] = map[uint64]ReceptionCallback{id1: cb} - - received, err := m.getCallback(tag, id1, false) - if err != nil { - t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id1, err) - } - - if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(received).Pointer() { - t.Errorf("Wrong callback.\nexpected: %p\nreceived: %p", cb, received) - } - - // Add new callback under the same tag but with deleteCB set to true and - // check that it is returned by getCallback and that it was deleted from the - // map while id1 was not - id2 := uint64(56) - cb = func([]byte) {} - m.callbacks[tag][id2] = cb - - received, err = m.getCallback(tag, id2, true) - if err != nil { - t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id2, err) - } - - if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(received).Pointer() { - t.Errorf("Wrong callback.\nexpected: %p\nreceived: %p", cb, received) - } - - received, err = m.getCallback(tag, id1, false) - if err != nil { - t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id1, err) - } - - received, err = m.getCallback(tag, id2, true) - if err == nil { - t.Errorf("getCallback did not get error when trying to get deleted "+ - "callback for tag %q and ID %d", tag, id2) - } -} - -// Tests that Manager.RegisterCallback registers a callback that is then called -// by Manager.processReceivedMessage. -func TestManager_RegisterCallback(t *testing.T) { - m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)} - - msg := Message{Tag: readyTag, ID: initID} - cbChan := make(chan struct{}, 1) - cb := func([]byte) { cbChan <- struct{}{} } - m.RegisterCallback(msg.Tag, cb) - - data, err := json.Marshal(msg) - if err != nil { - t.Fatalf("Failed to JSON marshal Message: %+v", err) - } - - go func() { - err = m.processReceivedMessage(data) - if err != nil { - t.Errorf("Failed to receive message: %+v", err) - } - }() - - select { - case <-cbChan: - case <-time.After(10 * time.Millisecond): - t.Error("Timed out waiting for callback to be called.") - } -} - -// Tests that Manager.registerReplyCallback registers a callback that is then -// called by Manager.processReceivedMessage. -func TestManager_registerReplyCallback(t *testing.T) { - m := &Manager{ - callbacks: make(map[Tag]map[uint64]ReceptionCallback), - responseIDs: make(map[Tag]uint64), - } - - msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}, 1) - cb := func([]byte) { cbChan <- struct{}{} } - m.registerReplyCallback(msg.Tag, cb) - m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb} - - data, err := json.Marshal(msg) - if err != nil { - t.Fatalf("Failed to JSON marshal Message: %+v", err) - } - - go func() { - err = m.processReceivedMessage(data) - if err != nil { - t.Errorf("Failed to receive message: %+v", err) - } - }() - - select { - case <-cbChan: - case <-time.After(10 * time.Millisecond): - t.Error("Timed out waiting for callback to be called.") - } -} - -// Tests that Manager.getNextID returns the expected ID for various Tags. -func TestManager_getNextID(t *testing.T) { - m := &Manager{ - callbacks: make(map[Tag]map[uint64]ReceptionCallback), - responseIDs: make(map[Tag]uint64), - } - - for _, tag := range []Tag{readyTag, "test", "A", "B", "C"} { - id := m.getNextID(tag) - if id != initID { - t.Errorf("ID for new tag %q is not initID."+ - "\nexpected: %d\nreceived: %d", tag, initID, id) - } - - for j := uint64(1); j < 100; j++ { - id = m.getNextID(tag) - if id != j { - t.Errorf("Unexpected ID for tag %q."+ - "\nexpected: %d\nreceived: %d", tag, j, id) - } - } - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Javascript Call Wrappers // -//////////////////////////////////////////////////////////////////////////////// - // Tests that newWorkerOptions returns a Javascript object with the expected // type, credentials, and name fields. func Test_newWorkerOptions(t *testing.T) { - for i, workerType := range []string{"classic", "module"} { - for j, credentials := range []string{"omit", "same-origin", "include"} { - for k, name := range []string{"name1", "name2", "name3"} { + for _, workerType := range []string{"classic", "module"} { + for _, credentials := range []string{"omit", "same-origin", "include"} { + for _, name := range []string{"name1", "name2", "name3"} { opts := newWorkerOptions(workerType, credentials, name) - v := opts.Get("type").String() - if v != workerType { - t.Errorf("Unexpected type (%d, %d, %d)."+ - "\nexpected: %s\nreceived: %s", i, j, k, workerType, v) + optsJS := js.ValueOf(opts) + + typeJS := optsJS.Get("type").String() + if typeJS != workerType { + t.Errorf("Unexected type (type:%s credentials:%s name:%s)"+ + "\nexpected: %s\nreceived: %s", + workerType, credentials, name, workerType, typeJS) } - v = opts.Get("credentials").String() - if v != credentials { - t.Errorf("Unexpected credentials (%d, %d, %d)."+ - "\nexpected: %s\nreceived: %s", i, j, k, credentials, v) + credentialsJS := optsJS.Get("credentials").String() + if typeJS != workerType { + t.Errorf("Unexected credentials (type:%s credentials:%s "+ + "name:%s)\nexpected: %s\nreceived: %s", workerType, + credentials, name, credentials, credentialsJS) } - v = opts.Get("name").String() - if v != name { - t.Errorf("Unexpected name (%d, %d, %d)."+ - "\nexpected: %s\nreceived: %s", i, j, k, name, v) + nameJS := optsJS.Get("name").String() + if typeJS != workerType { + t.Errorf("Unexected name (type:%s credentials:%s name:%s)"+ + "\nexpected: %s\nreceived: %s", + workerType, credentials, name, name, nameJS) } } } diff --git a/worker/message.go b/worker/message.go index 3d3bc23f131ef96cdc97a3e8684d1386b07cbeb2..c9de4c1d65b3674eacde808f8d776eb2085c6dbb 100644 --- a/worker/message.go +++ b/worker/message.go @@ -14,6 +14,6 @@ package worker type Message struct { Tag Tag `json:"tag"` ID uint64 `json:"id"` - DeleteCB bool `json:"deleteCB"` + Response bool `json:"response"` Data []byte `json:"data"` } diff --git a/worker/messageChannel.go b/worker/messageChannel.go new file mode 100644 index 0000000000000000000000000000000000000000..75de2938e38ff9bbb3ff4f0a3f3090fa91027f01 --- /dev/null +++ b/worker/messageChannel.go @@ -0,0 +1,103 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package worker + +import ( + "github.com/hack-pad/safejs" + "github.com/pkg/errors" + + "gitlab.com/elixxir/wasm-utils/utils" +) + +// MessageChannel wraps a Javascript MessageChannel object. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel +type MessageChannel struct { + safejs.Value +} + +// NewMessageChannel returns a new MessageChannel object with two new +// MessagePort objects. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel/MessageChannel +func NewMessageChannel() (MessageChannel, error) { + v, err := jsMessageChannel.New() + if err != nil { + return MessageChannel{}, err + } + return MessageChannel{v}, nil +} + +// CreateMessageChannel creates a new Javascript MessageChannel between two +// workers. The [Channel] tag will be used as the prefix in the name of the +// MessageChannel when printing to logs. The key is used to look up the callback +// registered on the worker to handle the MessageChannel creation. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel +func CreateMessageChannel(w1, w2 *Manager, channelName, key string) error { + // Create a Javascript MessageChannel + mc, err := NewMessageChannel() + if err != nil { + return err + } + channelNameJS := utils.CopyBytesToJS([]byte(channelName)) + keyJS := utils.CopyBytesToJS([]byte(key)) + + port1, err := mc.Port1() + if err != nil { + return errors.Wrap(err, "could not get port1") + } + + port2, err := mc.Port2() + if err != nil { + return errors.Wrap(err, "could not get port2") + } + + obj1 := map[string]any{ + "port": port1.Value, "channel": channelNameJS, "key": keyJS} + err = w1.w.PostMessageTransfer(obj1, port1.Value) + if err != nil { + return errors.Wrap(err, "failed to send port1") + } + + obj2 := map[string]any{ + "port": port2.Value, "channel": channelNameJS, "key": keyJS} + err = w2.w.PostMessageTransfer(obj2, port2.Value) + if err != nil { + return errors.Wrap(err, "failed to send port2") + } + + return nil +} + +// Port1 returns the first port of the message channel — the port attached to +// the context that originated the channel. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel/port1 +func (mc MessageChannel) Port1() (MessagePort, error) { + v, err := mc.Get("port1") + if err != nil { + return MessagePort{}, err + } + return NewMessagePort(v) +} + +// Port2 returns the second port of the message channel — the port attached to +// the context at the other end of the channel, which the message is initially +// sent to. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel/port2 +func (mc MessageChannel) Port2() (MessagePort, error) { + v, err := mc.Get("port2") + if err != nil { + return MessagePort{}, err + } + return NewMessagePort(v) +} diff --git a/worker/messageEvent.go b/worker/messageEvent.go new file mode 100644 index 0000000000000000000000000000000000000000..cf924a2848ddfc5f47554f1e280a1f3655b8ec98 --- /dev/null +++ b/worker/messageEvent.go @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package worker + +import ( + "github.com/hack-pad/safejs" + "github.com/pkg/errors" +) + +// MessageEvent is received from the channel returned by Listen(). +// Represents a JS MessageEvent. +type MessageEvent struct { + data safejs.Value + err error + target MessagePort +} + +// Data returns this event's data or a parse error +func (e MessageEvent) Data() (safejs.Value, error) { + return e.data, errors.Wrapf(e.err, "failed to parse MessageEvent %+v", e.data) +} + +func parseMessageEvent(v safejs.Value) MessageEvent { + value, err := v.Get("target") + if err != nil { + return MessageEvent{err: err} + } + + target, err := NewMessagePort(value) + if err != nil { + return MessageEvent{err: err} + } + + data, err := v.Get("data") + if err != nil { + return MessageEvent{err: err} + } + + return MessageEvent{ + data: data, + target: target, + } +} diff --git a/worker/messageManager.go b/worker/messageManager.go new file mode 100644 index 0000000000000000000000000000000000000000..e70369c3829e886a4d585d6a3eabf430c2152d2c --- /dev/null +++ b/worker/messageManager.go @@ -0,0 +1,419 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package worker + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "syscall/js" + "time" + + "github.com/aquilax/truncate" + "github.com/hack-pad/safejs" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" +) + +// SenderCallback is called when the sender of a message gets a response. The +// message is the response from the receiver. +type SenderCallback func(message []byte) + +// ReceiverCallback is called when receiving a message from the sender. Reply +// can optionally be used to send a response to the caller, triggering the +// [SenderCallback]. +type ReceiverCallback func(message []byte, reply func(message []byte)) + +// NewPortCallback is called with a MessagePort Javascript object when received. +type NewPortCallback func(port js.Value, channelName string) + +// MessageManager manages the sending and receiving of messages to a remote +// browser context (e.g., Worker and MessagePort) +type MessageManager struct { + // The underlying Javascript object that sends and receives messages. + p MessagePort + + // senderCallbacks are a list of SenderCallback that are called when + // receiving a response. The uint64 is a unique ID that connects each + // received reply to its original message. + senderCallbacks map[Tag]map[uint64]SenderCallback + + // receiverCallbacks are a list of ReceiverCallback that are called when + // receiving a message. + receiverCallbacks map[Tag]ReceiverCallback + + // responseIDs is a list of the newest ID to assign to each senderCallbacks + // when registered. The IDs are used to connect a reply to the original + // message. + responseIDs map[Tag]uint64 + + // messageChannelCB is a list of callbacks that are called when a new + // message channel is received. + messageChannelCB map[string]NewPortCallback + + // quit, when triggered, stops the thread that processes received messages. + quit chan struct{} + + // name names the underlying Javascript object. It is used for debugging and + // logging purposes. + name string + + Params + + mux sync.Mutex +} + +// NewMessageManager generates a new MessageManager. This functions will only +// return once communication with the remote thread has been established. +// TODO: test +func NewMessageManager( + v safejs.Value, name string, p Params) (*MessageManager, error) { + mm := initMessageManager(name, p) + mp, err := NewMessagePort(v) + if err != nil { + return nil, errors.Wrap(err, "invalid MessagePort value") + } + mm.p = mp + + ctx, cancel := context.WithCancel(context.Background()) + events, err := mm.p.Listen(ctx) + if err != nil { + cancel() + return nil, err + } + + // Start thread to process responses + go mm.messageReception(events, cancel) + + return mm, nil +} + +// initMessageManager initialises a new empty MessageManager. +func initMessageManager(name string, p Params) *MessageManager { + return &MessageManager{ + senderCallbacks: make(map[Tag]map[uint64]SenderCallback), + receiverCallbacks: make(map[Tag]ReceiverCallback), + responseIDs: make(map[Tag]uint64), + messageChannelCB: make(map[string]NewPortCallback), + quit: make(chan struct{}), + name: name, + Params: p, + } +} + +// Send sends the data to the remote thread with the given tag and waits for a +// response. Returns an error if calling postMessage throws an exception, +// marshalling the message to send fails, or if receiving a response times out. +// +// It is preferable to use [Send] over [SendNoResponse] as it will report a +// timeout when the remote thread crashes and [SendNoResponse] will not. +// TODO: test +func (mm *MessageManager) Send(tag Tag, data []byte) (response []byte, err error) { + return mm.SendTimeout(tag, data, mm.ResponseTimeout) +} + +// SendTimeout sends the data to the remote thread with a custom timeout. Refer +// to [Send] for more information. +// TODO: test +func (mm *MessageManager) SendTimeout( + tag Tag, data []byte, timeout time.Duration) (response []byte, err error) { + responseCh := make(chan []byte) + id := mm.registerSenderCallback(tag, func(msg []byte) { responseCh <- msg }) + + err = mm.sendMessage(tag, id, data) + if err != nil { + return nil, err + } + + select { + case response = <-responseCh: + return response, nil + case <-time.After(timeout): + return nil, + errors.Errorf("timed out after %s waiting for response", timeout) + } +} + +// SendNoResponse sends the data to the remote thread with the given tag; +// however, unlike [Send], it returns immediately and does not wait for a +// response. Returns an error if calling postMessage throws an exception, +// marshalling the message to send fails, or if receiving a response times out. +// +// It is preferable to use [Send] over [SendNoResponse] as it will report a +// timeout when the remote thread crashes and [SendNoResponse] will not. +// TODO: test +func (mm *MessageManager) SendNoResponse(tag Tag, data []byte) error { + return mm.sendMessage(tag, initID, data) +} + +// sendMessage packages the data into a Message with the tag and ID and sends it +// to the remote thread. +// TODO: test +func (mm *MessageManager) sendMessage(tag Tag, id uint64, data []byte) error { + if mm.MessageLogging { + jww.DEBUG.Printf("[WW] [%s] Sending message for %q and ID %d: %s", + mm.name, tag, id, truncate.Truncate( + fmt.Sprintf("%q", data), 64, "...", truncate.PositionMiddle)) + } + + msg := Message{ + Tag: tag, + ID: id, + Response: false, + Data: data, + } + payload, err := json.Marshal(msg) + if err != nil { + return err + } + + return mm.p.PostMessageTransferBytes(payload) +} + +// sendResponse sends a reply to the remote thread with the given tag and ID. +// TODO: test +func (mm *MessageManager) sendResponse(tag Tag, id uint64, data []byte) error { + if mm.MessageLogging { + jww.DEBUG.Printf("[WW] [%s] Sending reply for %q and ID %d: %s", + mm.name, tag, id, truncate.Truncate( + fmt.Sprintf("%q", data), 64, "...", truncate.PositionMiddle)) + } + + msg := Message{ + Tag: tag, + ID: id, + Response: true, + Data: data, + } + + payload, err := json.Marshal(msg) + if err != nil { + return err + } + + return mm.p.PostMessageTransferBytes(payload) +} + +// messageReception processes received messages sequentially. +// TODO: test +func (mm *MessageManager) messageReception( + events <-chan MessageEvent, cancel context.CancelFunc) { + jww.INFO.Printf("[WW] [%s] Starting message reception thread.", mm.name) + for { + select { + case <-mm.quit: + cancel() + jww.INFO.Printf( + "[WW] [%s] Quitting message reception thread.", mm.name) + return + case event := <-events: + + safeData, err := event.Data() + if err != nil { + exception.Throwf("Failed to process message: %+v", err) + } + data := safejs.Unsafe(safeData) + + switch data.Type() { + case js.TypeObject: + if data.Get("constructor").Equal(utils.Uint8Array) { + err = mm.processReceivedMessage(utils.CopyBytesToGo(data)) + if err != nil { + jww.ERROR.Printf("[WW] [%s] Failed to process "+ + "received message: %+v", mm.name, err) + } + break + } else if port := data.Get("port"); port.Truthy() { + err = mm.processReceivedPort(port, data) + if err != nil { + jww.ERROR.Printf("[WW] [%s] Failed to process "+ + "received MessagePort: %+v", mm.name, err) + } + break + } + fallthrough + + default: + jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %q: %s", + mm.name, data.Type(), utils.JsToJson(data)) + } + } + } +} + +// processReceivedMessage processes the received message and calls the +// associated callback. This functions blocks until the callback returns. +func (mm *MessageManager) processReceivedMessage(data []byte) error { + var msg Message + err := json.Unmarshal(data, &msg) + if err != nil { + return err + } + + if mm.MessageLogging { + jww.DEBUG.Printf("[WW] [%s] Received message for %q and ID %d "+ + "with data: %s", mm.name, msg.Tag, msg.ID, truncate.Truncate( + fmt.Sprintf("%q", data), 64, "...", truncate.PositionMiddle)) + } + + if msg.Response { + callback, err := mm.getSenderCallback(msg.Tag, msg.ID) + if err != nil { + return err + } + + callback(msg.Data) + } else { + callback, err := mm.getReceiverCallback(msg.Tag) + if err != nil { + return err + } + + callback(msg.Data, func(message []byte) { + if err = mm.sendResponse(msg.Tag, msg.ID, message); err != nil { + jww.FATAL.Panicf("[WW] [%s] Failed to send response for %q "+ + "and ID %d: %+v", mm.name, msg.Tag, msg.ID, err) + } + }) + } + + return nil +} + +// processReceivedPort processes the received Javascript MessagePort and calls +// the associated NewPortCallback callback. This functions blocks until the +// callback returns. +func (mm *MessageManager) processReceivedPort(port js.Value, data js.Value) error { + channel := string(utils.CopyBytesToGo(data.Get("channel"))) + key := string(utils.CopyBytesToGo(data.Get("key"))) + + jww.INFO.Printf("[WW] [%s] Received new MessageChannel %q for key %q.", + mm.name, channel, key) + + mm.mux.Lock() + cb, exists := mm.messageChannelCB[key] + mm.mux.Unlock() + + if !exists { + return errors.Errorf( + "Failed to find callback for channel %q and key %q.", channel, key) + } else { + cb(port, channel) + } + + return nil +} + +// RegisterCallback registers the callback for the given tag. Previous tags are +// overwritten. This function is thread safe. +func (mm *MessageManager) RegisterCallback(tag Tag, receiverCB ReceiverCallback) { + mm.mux.Lock() + defer mm.mux.Unlock() + + jww.DEBUG.Printf("[WW] [%s] Registering receiver callback for tag %q", + mm.name, tag) + + mm.receiverCallbacks[tag] = receiverCB +} + +// getReceiverCallback returns the ReceiverCallback for the given Tag or returns +// an error if no callback is found. This function is thread safe. +func (mm *MessageManager) getReceiverCallback(tag Tag) (ReceiverCallback, error) { + mm.mux.Lock() + defer mm.mux.Unlock() + + callback, exists := mm.receiverCallbacks[tag] + if !exists { + return nil, errors.Errorf("no receiver callbacks found for tag %q", tag) + } + + return callback, nil +} + +// registerSenderCallback registers the callback for the given tag and a new +// unique ID used to associate the reply to the callback. Returns the ID that +// was registered. If a previous callback was registered, it is overwritten. +// This function is thread safe. +func (mm *MessageManager) registerSenderCallback( + tag Tag, senderCB SenderCallback) uint64 { + mm.mux.Lock() + defer mm.mux.Unlock() + id := mm.getNextID(tag) + + jww.DEBUG.Printf("[WW] [%s] Registering callback for tag %q and ID %d", + mm.name, tag, id) + + if _, exists := mm.senderCallbacks[tag]; !exists { + mm.senderCallbacks[tag] = make(map[uint64]SenderCallback) + } + mm.senderCallbacks[tag][id] = senderCB + + return id +} + +// getSenderCallback returns the SenderCallback for the given Tag and ID or +// returns an error if no callback is found. The callback is deleted from the +// map once found. This function is thread safe. +func (mm *MessageManager) getSenderCallback( + tag Tag, id uint64) (SenderCallback, error) { + mm.mux.Lock() + defer mm.mux.Unlock() + callbacks, exists := mm.senderCallbacks[tag] + if !exists { + return nil, errors.Errorf("no sender callbacks found for tag %q", tag) + } + + callback, exists := callbacks[id] + if !exists { + return nil, + errors.Errorf("no %q sender callback found for ID %d", tag, id) + } + + delete(mm.senderCallbacks[tag], id) + if len(mm.senderCallbacks[tag]) == 0 { + delete(mm.senderCallbacks, tag) + } + + return callback, nil +} + +// RegisterMessageChannelCallback registers a callback that will be called when +// a MessagePort with the given Channel is received. +func (mm *MessageManager) RegisterMessageChannelCallback( + key string, fn NewPortCallback) { + mm.mux.Lock() + defer mm.mux.Unlock() + mm.messageChannelCB[key] = fn +} + +// Stop closes the message reception thread and closes the port. +// TODO: test +func (mm *MessageManager) Stop() { + // Stop messageReception + select { + case mm.quit <- struct{}{}: + } +} + +// getNextID returns the next unique ID for the given tag. This function is not +// thread-safe. +func (mm *MessageManager) getNextID(tag Tag) uint64 { + if _, exists := mm.responseIDs[tag]; !exists { + mm.responseIDs[tag] = initID + } + + id := mm.responseIDs[tag] + mm.responseIDs[tag]++ + return id +} diff --git a/worker/messageManager_test.go b/worker/messageManager_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bd01f64a3a50b3d1d246fa8cd85c04eda94a4899 --- /dev/null +++ b/worker/messageManager_test.go @@ -0,0 +1,344 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package worker + +import ( + "encoding/json" + "github.com/hack-pad/safejs" + "gitlab.com/elixxir/wasm-utils/utils" + "reflect" + "strconv" + "syscall/js" + "testing" + "time" +) + +func TestNewMessageManager(t *testing.T) { +} + +// Unit test of initMessageManager. +func Test_initMessageManager(t *testing.T) { + expected := &MessageManager{ + senderCallbacks: make(map[Tag]map[uint64]SenderCallback), + receiverCallbacks: make(map[Tag]ReceiverCallback), + responseIDs: make(map[Tag]uint64), + messageChannelCB: make(map[string]NewPortCallback), + quit: make(chan struct{}), + name: "name", + Params: DefaultParams(), + } + + received := initMessageManager(expected.name, expected.Params) + + received.quit = expected.quit + if !reflect.DeepEqual(expected, received) { + t.Errorf("Unexpected MessageManager.\nexpected: %+v\nreceived: %+v", + expected, received) + } +} + +func TestMessageManager_Send(t *testing.T) { +} + +func TestMessageManager_SendTimeout(t *testing.T) { +} + +func TestMessageManager_SendNoResponse(t *testing.T) { +} + +func TestMessageManager_sendMessage(t *testing.T) { +} + +func TestMessageManager_sendResponse(t *testing.T) { +} + +func TestMessageManager_messageReception(t *testing.T) { +} + +// Tests MessageManager.processReceivedMessage calls the expected callback. +func TestMessageManager_processReceivedMessage(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + msg := Message{Tag: readyTag, ID: 5} + cbChan := make(chan struct{}) + cb := func([]byte, func([]byte)) { cbChan <- struct{}{} } + mm.RegisterCallback(msg.Tag, cb) + + data, err := json.Marshal(msg) + if err != nil { + t.Fatalf("Failed to JSON marshal Message: %+v", err) + } + + go func() { + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + err = mm.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } + + msg = Message{Tag: "tag", Response: true} + cbChan = make(chan struct{}) + cb2 := func([]byte) { cbChan <- struct{}{} } + mm.registerSenderCallback(msg.Tag, cb2) + + go func() { + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + err = mm.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } +} + +// Tests MessageManager.processReceivedPort calls the expected callback. +func TestMessageManager_processReceivedPort(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + cbChan := make(chan string) + cb := func(port js.Value, channelName string) { cbChan <- channelName } + key := "testKey" + channelName := "channel" + mm.RegisterMessageChannelCallback(key, cb) + + mc, err := NewMessageChannel() + if err != nil { + t.Fatal(err) + } + port1, err := mc.Port1() + if err != nil { + t.Fatalf("Failed to get port1: %+v", err) + } + + obj := map[string]any{ + "port": safejs.Unsafe(port1.Value), + "channel": utils.CopyBytesToJS([]byte(channelName)), + "key": utils.CopyBytesToJS([]byte(key))} + + go func() { + select { + case name := <-cbChan: + if channelName != name { + t.Errorf("Received incorrect channel name."+ + "\nexpected: %q\nrecieved: %q", channelName, name) + } + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + data := js.ValueOf(obj) + err = mm.processReceivedPort(data.Get("port"), data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } +} + +// Tests that MessageManager.RegisterCallback registers a callback that is then +// called by MessageManager.processReceivedMessage. +func TestMessageManager_RegisterCallback(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + msg := Message{Tag: readyTag, ID: initID} + cbChan := make(chan struct{}) + cb := func([]byte, func([]byte)) { cbChan <- struct{}{} } + mm.RegisterCallback(msg.Tag, cb) + + data, err := json.Marshal(msg) + if err != nil { + t.Fatalf("Failed to JSON marshal Message: %+v", err) + } + + go func() { + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + err = mm.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } +} + +// Tests MessageManager.getReceiverCallback returns the expected callback. +func TestMessageManager_getReceiverCallback(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + expected := make(map[Tag]ReceiverCallback) + for i := 0; i < 5; i++ { + tag := Tag("tag" + strconv.Itoa(i)) + cb := func([]byte, func([]byte)) {} + mm.RegisterCallback(tag, cb) + expected[tag] = cb + } + + for tag, cb := range expected { + r, err := mm.getReceiverCallback(tag) + if err != nil { + t.Errorf("Error getting callback for tag %q: %+v", tag, err) + } + + if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(r).Pointer() { + t.Errorf("Wrong callback for tag %q."+ + "\nexpected: %p\nreceived: %p", tag, cb, r) + } + } +} + +// Tests that MessageManager.registerSenderCallback registers a callback that is +// then called by MessageManager.processReceivedMessage. +func TestMessageManager_registerSenderCallback(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + msg := Message{Tag: readyTag, Response: true} + cbChan := make(chan struct{}) + cb := func([]byte) { cbChan <- struct{}{} } + msg.ID = mm.registerSenderCallback(msg.Tag, cb) + + data, err := json.Marshal(msg) + if err != nil { + t.Fatalf("Failed to JSON marshal Message: %+v", err) + } + + go func() { + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + err = mm.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } +} + +// Tests MessageManager.getSenderCallback returns the expected callback and +// deletes it. +func TestMessageManager_getSenderCallback(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + expected := make(map[Tag]map[uint64]SenderCallback) + for i := 0; i < 5; i++ { + tag := Tag("tag" + strconv.Itoa(i)) + expected[tag] = make(map[uint64]SenderCallback) + for j := 0; j < 10; j++ { + cb := func([]byte) {} + id := mm.registerSenderCallback(tag, cb) + expected[tag][id] = cb + } + } + + for tag, callbacks := range expected { + for id, cb := range callbacks { + r, err := mm.getSenderCallback(tag, id) + if err != nil { + t.Errorf("Error getting callback for tag %q and ID %d: %+v", + tag, id, err) + } + + if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(r).Pointer() { + t.Errorf("Wrong callback for tag %q and ID %d."+ + "\nexpected: %p\nreceived: %p", tag, id, cb, r) + } + + // Check that the callback was deleted + if r, err = mm.getSenderCallback(tag, id); err == nil { + t.Errorf("Did not get error when for callback that should be "+ + "deleted for tag %q and ID %d: %p", tag, id, r) + } + } + if callbacks, exists := mm.senderCallbacks[tag]; exists { + t.Errorf("Empty map for tag %s not deleted: %+v", tag, callbacks) + } + } +} + +// Tests that MessageManager.RegisterMessageChannelCallback registers a callback +// that is then called by MessageManager.processReceivedPort. +func TestMessageManager_RegisterMessageChannelCallback(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + cbChan := make(chan string) + cb := func(port js.Value, channelName string) { cbChan <- channelName } + key := "testKey" + channelName := "channel" + mm.RegisterMessageChannelCallback(key, cb) + + mc, err := NewMessageChannel() + if err != nil { + t.Fatal(err) + } + port1, err := mc.Port1() + if err != nil { + t.Fatalf("Failed to get port1: %+v", err) + } + + obj := map[string]any{ + "port": safejs.Unsafe(port1.Value), + "channel": utils.CopyBytesToJS([]byte(channelName)), + "key": utils.CopyBytesToJS([]byte(key))} + + go func() { + select { + case name := <-cbChan: + if channelName != name { + t.Errorf("Received incorrect channel name."+ + "\nexpected: %q\nrecieved: %q", channelName, name) + } + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + data := js.ValueOf(obj) + err = mm.processReceivedPort(data.Get("port"), data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } +} + +func TestMessageManager_Stop(t *testing.T) { +} + +// Tests that MessageManager.getNextID returns the expected ID for various Tags. +func TestMessageManager_getNextID(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + for _, tag := range []Tag{readyTag, "test", "A", "B", "C"} { + id := mm.getNextID(tag) + if id != initID { + t.Errorf("ID for new tag %q is not initID."+ + "\nexpected: %d\nreceived: %d", tag, initID, id) + } + + for j := initID + 1; j < 100; j++ { + id = mm.getNextID(tag) + if id != j { + t.Errorf("Unexpected ID for tag %q."+ + "\nexpected: %d\nreceived: %d", tag, j, id) + } + } + } +} diff --git a/worker/messagePort.go b/worker/messagePort.go new file mode 100644 index 0000000000000000000000000000000000000000..10724555a9b439776bdcdb604640da2590138f4b --- /dev/null +++ b/worker/messagePort.go @@ -0,0 +1,134 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package worker + +import ( + "context" + "syscall/js" + + "github.com/hack-pad/safejs" + "github.com/pkg/errors" + + "gitlab.com/elixxir/wasm-utils/utils" +) + +// MessagePort wraps a Javascript MessagePort object. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort +type MessagePort struct { + safejs.Value +} + +// NewMessagePort wraps the given MessagePort. +func NewMessagePort(v safejs.Value) (MessagePort, error) { + method, err := v.Get("postMessage") + if err != nil { + return MessagePort{}, err + } + if method.Type() != safejs.TypeFunction { + return MessagePort{}, errors.New("postMessage is not a function") + } + return MessagePort{v}, nil +} + +// PostMessage sends a message from the port. +func (mp MessagePort) PostMessage(message any) error { + _, err := mp.Call("postMessage", message) + return err +} + +// PostMessageTransfer sends a message from the port and transfers ownership of +// objects to other browsing contexts. +func (mp MessagePort) PostMessageTransfer(message any, transfer ...any) error { + _, err := mp.Call("postMessage", message, transfer) + return err +} + +// PostMessageTransferBytes sends the message bytes from the port via transfer. +func (mp MessagePort) PostMessageTransferBytes(message []byte) error { + buffer := utils.CopyBytesToJS(message) + return mp.PostMessageTransfer(buffer, buffer.Get("buffer")) +} + +// Listen registers listeners on the MessagePort and returns all events on the +// returned channel. +func (mp MessagePort) Listen( + ctx context.Context) (_ <-chan MessageEvent, err error) { + ctx, cancel := context.WithCancel(ctx) + defer func() { + if err != nil { + cancel() + } + }() + + events := make(chan MessageEvent) + messageHandler, err := nonBlocking(func(args []safejs.Value) { + events <- parseMessageEvent(args[0]) + }) + if err != nil { + return nil, err + } + errorHandler, err := nonBlocking(func(args []safejs.Value) { + events <- MessageEvent{err: js.Error{Value: safejs.Unsafe(args[0])}} + }) + if err != nil { + return nil, err + } + messageErrorHandler, err := nonBlocking(func(args []safejs.Value) { + events <- parseMessageEvent(args[0]) + }) + if err != nil { + return nil, err + } + + go func() { + <-ctx.Done() + _, err := mp.Call("removeEventListener", "message", messageHandler) + if err == nil { + messageHandler.Release() + } + _, err = mp.Call("removeEventListener", "error", errorHandler) + if err == nil { + errorHandler.Release() + } + _, err = mp.Call("removeEventListener", "messageerror", messageErrorHandler) + if err == nil { + messageErrorHandler.Release() + } + close(events) + }() + _, err = mp.Call("addEventListener", "message", messageHandler) + if err != nil { + return nil, err + } + _, err = mp.Call("addEventListener", "error", errorHandler) + if err != nil { + return nil, err + } + _, err = mp.Call("addEventListener", "messageerror", messageErrorHandler) + if err != nil { + return nil, err + } + if start, err := mp.Get("start"); err == nil { + if truthy, err := start.Truthy(); err == nil && truthy { + if _, err := mp.Call("start"); err != nil { + return nil, err + } + } + } + return events, nil +} + +func nonBlocking(fn func(args []safejs.Value)) (safejs.Func, error) { + return safejs.FuncOf(func(_ safejs.Value, args []safejs.Value) any { + go fn(args) + return nil + }) +} diff --git a/worker/params.go b/worker/params.go new file mode 100644 index 0000000000000000000000000000000000000000..adddb34860381018d77ba28511f5cadefb7a48ac --- /dev/null +++ b/worker/params.go @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package worker + +import "time" + +// Params are parameters used in the [MessageManager]. +type Params struct { + // MessageLogging indicates if a DEBUG message should be printed every time + // a message is sent or received. + MessageLogging bool + + // ResponseTimeout is the default timeout to wait for a response before + // timing out and returning an error. + ResponseTimeout time.Duration +} + +// DefaultParams returns the default parameters. +func DefaultParams() Params { + return Params{ + MessageLogging: false, + ResponseTimeout: 30 * time.Second, + } +} diff --git a/worker/tag.go b/worker/tag.go index fa458c291802c47d0ffb1970ef75202b936561a4..3bb27a08e30241b233bec63225eac3e266fda9f7 100644 --- a/worker/tag.go +++ b/worker/tag.go @@ -14,5 +14,10 @@ type Tag string // Generic tags used by all workers. const ( - readyTag Tag = "Ready" + readyTag Tag = "<WW>Ready</WW>" +) + +const ( + Channel1LogMsgChanName = "Channel1Logger" + LoggerTag = "logger" ) diff --git a/worker/thread.go b/worker/thread.go index 393ff7661db3f0c773932fdff932a15a3bfe92b9..b67c3464c885bf129aeffef44fef1683c44881f7 100644 --- a/worker/thread.go +++ b/worker/thread.go @@ -10,287 +10,151 @@ package worker import ( - "encoding/json" - "sync" "syscall/js" + "time" + "github.com/hack-pad/safejs" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" - - "gitlab.com/elixxir/wasm-utils/utils" ) // ThreadReceptionCallback is called with a message received from the main // thread. Any bytes returned are sent as a response back to the main thread. // Any returned errors are printed to the log. -type ThreadReceptionCallback func(data []byte) ([]byte, error) +type ThreadReceptionCallback func(message []byte, reply func(message []byte)) // ThreadManager queues incoming messages from the main thread and handles them // based on their tag. type ThreadManager struct { - // messages is a list of queued messages sent from the main thread. - messages chan js.Value - - // callbacks is a list of callbacks to handle messages that come from the - // main thread keyed on the callback tag. - callbacks map[Tag]ThreadReceptionCallback - - // receiveQueue is the channel that all received MessageEvent.data are - // queued on while they wait to be processed. - receiveQueue chan js.Value - - // quit, when triggered, stops the thread that processes received messages. - quit chan struct{} + mm *MessageManager - // name describes the worker. It is used for debugging and logging purposes. - name string - - // messageLogging determines if debug message logs should be printed every - // time a message is sent/received to/from the worker. - messageLogging bool - - mux sync.Mutex + // Wrapper of the DedicatedWorkerGlobalScope. + // Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope + t Thread } // NewThreadManager initialises a new ThreadManager. -func NewThreadManager(name string, messageLogging bool) *ThreadManager { - tm := &ThreadManager{ - messages: make(chan js.Value, 100), - callbacks: make(map[Tag]ThreadReceptionCallback), - receiveQueue: make(chan js.Value, receiveQueueChanSize), - quit: make(chan struct{}), - name: name, - messageLogging: messageLogging, +func NewThreadManager(name string, messageLogging bool) (*ThreadManager, error) { + t, err := NewThread() + if err != nil { + return nil, errors.Wrapf(err, "failed to construct GlobalSelf") } - // Start thread to process messages from the main thread - go tm.processThread() - - tm.addEventListeners() - return tm -} + p := DefaultParams() + p.MessageLogging = messageLogging + mm, err := NewMessageManager(t.Value, name+"-remote", p) + if err != nil { + return nil, errors.Wrapf(err, "failed to construct message manager") + } -// Stop closes the thread manager and stops the worker. -func (tm *ThreadManager) Stop() { - // Stop processThread - select { - case tm.quit <- struct{}{}: + tm := &ThreadManager{ + mm: mm, + t: t, } - // Terminate the worker - go tm.close() + return tm, nil } -// processThread processes received messages sequentially. -func (tm *ThreadManager) processThread() { - jww.INFO.Printf("[WW] [%s] Starting worker process thread.", tm.name) - for { - select { - case <-tm.quit: - jww.INFO.Printf("[WW] [%s] Quitting worker process thread.", tm.name) - return - case msgData := <-tm.receiveQueue: +// Stop closes the thread manager and stops the worker. +func (tm *ThreadManager) Stop() error { + tm.mm.Stop() - switch msgData.Type() { - case js.TypeObject: - if msgData.Get("constructor").Equal(utils.Uint8Array) { - err := tm.processReceivedMessage(utils.CopyBytesToGo(msgData)) - if err != nil { - jww.ERROR.Printf("[WW] [%s] Failed to process message "+ - "received from main thread: %+v", tm.name, err) - } - break - } - fallthrough + // Close the worker + err := tm.t.Close() + return errors.Wrapf(err, "failed to close worker %q", tm.mm.name) +} - default: - jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+ - "from main thread: %s", - tm.name, msgData.Type(), utils.JsToJson(msgData)) - } - } - } +func (tm *ThreadManager) GetWorker() js.Value { + return safejs.Unsafe(tm.t.Value) } // SignalReady sends a signal to the main thread indicating that the worker is // ready. Once the main thread receives this, it will initiate communication. // Therefore, this should only be run once all listeners are ready. func (tm *ThreadManager) SignalReady() { - tm.SendMessage(readyTag, nil) -} - -// SendMessage sends a message to the main thread for the given tag. -func (tm *ThreadManager) SendMessage(tag Tag, data []byte) { - msg := Message{ - Tag: tag, - ID: initID, - DeleteCB: false, - Data: data, - } - - if tm.messageLogging { - jww.DEBUG.Printf("[WW] [%s] Worker sending message for %q with data: %s", - tm.name, tag, data) - } - - payload, err := json.Marshal(msg) + err := tm.mm.SendNoResponse(readyTag, nil) if err != nil { - jww.FATAL.Panicf("[WW] [%s] Worker failed to marshal %T for %q going "+ - "to main: %+v", tm.name, msg, tag, err) + jww.FATAL.Panicf( + "[WW] [%s] Failed to send ready signal: %+v", tm.Name(), err) } - - go tm.postMessage(payload) } -// sendResponse sends a reply to the main thread with the given tag and ID. -func (tm *ThreadManager) sendResponse(tag Tag, id uint64, data []byte) error { - msg := Message{ - Tag: tag, - ID: id, - DeleteCB: true, - Data: data, - } - - if tm.messageLogging { - jww.DEBUG.Printf("[WW] [%s] Worker sending reply for %q and ID %d "+ - "with data: %s", tm.name, tag, id, data) - } - - payload, err := json.Marshal(msg) - if err != nil { - return errors.Errorf("worker failed to marshal %T for %q and ID "+ - "%d going to main: %+v", msg, tag, id, err) - } - - go tm.postMessage(payload) - - return nil +// RegisterMessageChannelCallback registers a callback that will be called when +// a MessagePort with the given Channel is received. +func (tm *ThreadManager) RegisterMessageChannelCallback( + tag string, fn NewPortCallback) { + tm.mm.RegisterMessageChannelCallback(tag, fn) } -// receiveMessage is registered with the Javascript event listener and is called -// every time a new message from the main thread is received. -func (tm *ThreadManager) receiveMessage(data js.Value) { - tm.receiveQueue <- data +// SendMessage sends a message to the main thread with the given tag and waits +// for a response. An error is returned on failure to send or on timeout. +func (tm *ThreadManager) SendMessage( + tag Tag, data []byte) (response []byte, err error) { + return tm.mm.Send(tag, data) } -// processReceivedMessage processes the message received from the main thread -// and calls the associated callback. If the registered callback returns a -// response, it is sent to the main thread. This functions blocks until the -// callback returns. -func (tm *ThreadManager) processReceivedMessage(data []byte) error { - var msg Message - err := json.Unmarshal(data, &msg) - if err != nil { - return err - } - - if tm.messageLogging { - jww.DEBUG.Printf("[WW] [%s] Worker received message for %q and ID %d "+ - "with data: %s", tm.name, msg.Tag, msg.ID, msg.Data) - } - - tm.mux.Lock() - callback, exists := tm.callbacks[msg.Tag] - tm.mux.Unlock() - if !exists { - return errors.Errorf("no callback found for tag %q", msg.Tag) - } - - // Call callback and register response with its return - response, err := callback(msg.Data) - if err != nil { - return errors.Errorf("callback for %q and ID %d returned an error: %+v", - msg.Tag, msg.ID, err) - } - if response != nil { - return tm.sendResponse(msg.Tag, msg.ID, response) - } +// SendTimeout sends a message to the main thread with the given tag and waits +// for a response. An error is returned on failure to send or on the specified +// timeout. +func (tm *ThreadManager) SendTimeout( + tag Tag, data []byte, timeout time.Duration) (response []byte, err error) { + return tm.mm.SendTimeout(tag, data, timeout) +} - return nil +// SendNoResponse sends a message to the main thread with the given tag. It +// returns immediately and does not wait for a response. +func (tm *ThreadManager) SendNoResponse(tag Tag, data []byte) error { + return tm.mm.SendNoResponse(tag, data) } -// RegisterCallback registers the callback with the given tag overwriting any -// previous registered callbacks with the same tag. This function is thread -// safe. -// -// If the callback returns anything but nil, it will be returned as a response. -func (tm *ThreadManager) RegisterCallback( - tag Tag, receptionCallback ThreadReceptionCallback) { - jww.DEBUG.Printf( - "[WW] [%s] Worker registering callback for tag %q", tm.name, tag) - tm.mux.Lock() - tm.callbacks[tag] = receptionCallback - tm.mux.Unlock() +// RegisterCallback registers the callback for the given tag. Previous tags are +// overwritten. This function is thread safe. +func (tm *ThreadManager) RegisterCallback(tag Tag, receiverCB ReceiverCallback) { + tm.mm.RegisterCallback(tag, receiverCB) } +// Name returns the name of the web worker. +func (tm *ThreadManager) Name() string { return tm.mm.name } + //////////////////////////////////////////////////////////////////////////////// // Javascript Call Wrappers // //////////////////////////////////////////////////////////////////////////////// -// addEventListeners adds the event listeners needed to receive a message from -// the worker. Two listeners were added; one to receive messages from the worker -// and the other to receive errors. -func (tm *ThreadManager) addEventListeners() { - // Create a listener for when the message event is fire on the worker. This - // occurs when a message is received from the main thread. - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event - messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { - tm.receiveMessage(args[0].Get("data")) - return nil - }) +// Thread has the methods of the Javascript DedicatedWorkerGlobalScope. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope +type Thread struct { + MessagePort +} - // Create listener for when an error event is fired on the worker. This - // occurs when an error occurs in the worker. - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event - errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { - event := args[0] - jww.ERROR.Printf("[WW] [%s] Worker received error event: %+v", - tm.name, js.Error{Value: event}) - return nil - }) +// NewThread creates a new Thread from Global. +func NewThread() (Thread, error) { + self, err := safejs.Global().Get("self") + if err != nil { + return Thread{}, err + } - // Create listener for when a messageerror event is fired on the worker. - // This occurs when it receives a message that cannot be deserialized. - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event - messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { - event := args[0] - jww.ERROR.Printf("[WW] [%s] Worker received message error event: %+v", - tm.name, js.Error{Value: event}) - return nil - }) + mp, err := NewMessagePort(self) + if err != nil { + return Thread{}, err + } - // Register each event listener on the worker using addEventListener - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener - js.Global().Call("addEventListener", "message", messageEvent) - js.Global().Call("addEventListener", "error", errorEvent) - js.Global().Call("addEventListener", "messageerror", messageerrorEvent) + return Thread{mp}, nil } -// postMessage sends a message from this worker to the main WASM thread. +// Name returns the name that the Worker was (optionally) given when it was +// created. // -// aMessage must be a js.Value or a primitive type that can be converted via -// js.ValueOf. The Javascript object must be "any value or JavaScript object -// handled by the structured clone algorithm". See the doc for more information. - -// aMessage is the object to deliver to the main thread; this will be in the -// data field in the event delivered to the thread. It must be a transferable -// object because this function transfers ownership of the message instead of -// copying it for better performance. See the doc for more information. -// -// Doc: https://developer.mozilla.org/docs/Web/API/DedicatedWorkerGlobalScope/postMessage -func (tm *ThreadManager) postMessage(aMessage []byte) { - buffer := utils.CopyBytesToJS(aMessage) - js.Global().Call("postMessage", buffer, []any{buffer.Get("buffer")}) +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/name +func (t *Thread) Name() string { + return safejs.Unsafe(t.Value).Get("name").String() } -// close discards any tasks queued in the worker's event loop, effectively +// Close discards any tasks queued in the worker's event loop, effectively // closing this particular scope. // -// aMessage must be a js.Value or a primitive type that can be converted via -// js.ValueOf. The Javascript object must be "any value or JavaScript object -// handled by the structured clone algorithm". See the doc for more information. -// // Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/close -func (tm *ThreadManager) close() { - js.Global().Call("close") +func (t *Thread) Close() error { + _, err := t.Call("close") + return err } diff --git a/worker/thread_test.go b/worker/thread_test.go deleted file mode 100644 index d738580aad5d66ad4eab81ee934db52b24313825..0000000000000000000000000000000000000000 --- a/worker/thread_test.go +++ /dev/null @@ -1,73 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -//go:build js && wasm - -package worker - -import ( - "encoding/json" - "testing" - "time" -) - -// Tests that ThreadManager.processReceivedMessage calls the expected callback. -func TestThreadManager_processReceivedMessage(t *testing.T) { - tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)} - - msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}, 1) - cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil } - tm.callbacks[msg.Tag] = cb - - data, err := json.Marshal(msg) - if err != nil { - t.Fatalf("Failed to JSON marshal Message: %+v", err) - } - - go func() { - err = tm.processReceivedMessage(data) - if err != nil { - t.Errorf("Failed to receive message: %+v", err) - } - }() - - select { - case <-cbChan: - case <-time.After(10 * time.Millisecond): - t.Error("Timed out waiting for callback to be called.") - } -} - -// Tests that ThreadManager.RegisterCallback registers a callback that is then -// called by ThreadManager.processReceivedMessage. -func TestThreadManager_RegisterCallback(t *testing.T) { - tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)} - - msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}, 1) - cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil } - tm.RegisterCallback(msg.Tag, cb) - - data, err := json.Marshal(msg) - if err != nil { - t.Fatalf("Failed to JSON marshal Message: %+v", err) - } - - go func() { - err = tm.processReceivedMessage(data) - if err != nil { - t.Errorf("Failed to receive message: %+v", err) - } - }() - - select { - case <-cbChan: - case <-time.After(10 * time.Millisecond): - t.Error("Timed out waiting for callback to be called.") - } -}