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.")
-	}
-}