diff --git a/indexedDbWorker/channels/channelsIndexedDbWorker.js b/indexedDb/channels/channelsIndexedDbWorker.js similarity index 93% rename from indexedDbWorker/channels/channelsIndexedDbWorker.js rename to indexedDb/channels/channelsIndexedDbWorker.js index 7c03a409a1b5e62aab21a78dbffa7affa87b53b3..245c80d0368dc04805e9dadfcf95a7700693483b 100644 --- a/indexedDbWorker/channels/channelsIndexedDbWorker.js +++ b/indexedDb/channels/channelsIndexedDbWorker.js @@ -8,7 +8,7 @@ importScripts('wasm_exec.js'); const go = new Go(); -const binPath = 'xxdk-indexedDkWorker.wasm' +const binPath = 'channelsIndexedDkWorker.wasm' WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => { go.run(result.instance); }).catch((err) => { diff --git a/indexedDbWorker/channels/handlers.go b/indexedDb/channels/handlers.go similarity index 98% rename from indexedDbWorker/channels/handlers.go rename to indexedDb/channels/handlers.go index b4ba0fcbab623fbfab14f3393392150491749729..d9feece8492cc8beaca31c25404a6f8ea10c2c5b 100644 --- a/indexedDbWorker/channels/handlers.go +++ b/indexedDb/channels/handlers.go @@ -20,8 +20,8 @@ import ( "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/elixxir/crypto/message" "gitlab.com/elixxir/xxdk-wasm/indexedDb" - mChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/channels" "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" + mChannels "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker/channels" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/primitives/id" "time" @@ -30,7 +30,7 @@ import ( // manager handles the event model and the message handler, which is used to // send information between the event model and the main thread. type manager struct { - mh *indexedDbWorker.MessageHandler + mh *indexedDb2.MessageHandler model channels.EventModel } @@ -81,7 +81,7 @@ func (m *manager) newWASMEventModelHandler(data []byte) []byte { // messageReceivedCallback sends calls to the MessageReceivedCallback in the // main thread. // -// storeEncryptionStatus adhere to the indexedDb.MessageReceivedCallback type. +// storeEncryptionStatus adhere to the indexedDbWorker.MessageReceivedCallback type. func (m *manager) messageReceivedCallback( uuid uint64, channelID *id.ID, update bool) { // Package parameters for sending diff --git a/indexedDb/channels/implementation.go b/indexedDb/channels/implementation.go index 6538e4f76c7d894cce5b8373364b796f78106528..a971ec9e537621371945e29fa87180b7e479b777 100644 --- a/indexedDb/channels/implementation.go +++ b/indexedDb/channels/implementation.go @@ -7,22 +7,28 @@ //go:build js && wasm -package channels +package main import ( "crypto/ed25519" + "encoding/base64" "encoding/json" - "time" - "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "strings" + "sync" + "syscall/js" + "time" + "github.com/hack-pad/go-indexeddb/idb" "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" + cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/message" + "gitlab.com/elixxir/xxdk-wasm/utils" "gitlab.com/xx_network/primitives/id" ) @@ -30,23 +36,108 @@ import ( // system passed an object that adheres to in order to get events on the // channel. type wasmModel struct { - wh *indexedDb.WorkerHandler + db *idb.Database + cipher cryptoChannel.Cipher + receivedMessageCB MessageReceivedCallback + updateMux sync.Mutex } // JoinChannel is called whenever a channel is joined locally. func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { - data, err := json.Marshal(channel) + parentErr := errors.New("failed to JoinChannel") + + // Build object + newChannel := Channel{ + ID: channel.ReceptionID.Marshal(), + Name: channel.Name, + Description: channel.Description, + } + + // Convert to jsObject + newChannelJson, err := json.Marshal(&newChannel) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to marshal Channel: %+v", err)) + return + } + channelObj, err := utils.JsonToJS(newChannelJson) if err != nil { - jww.ERROR.Printf("Could not JSON marshal broadcast.Channel: %+v", err) + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to marshal Channel: %+v", err)) return } - w.wh.SendMessage(indexedDb.JoinChannelTag, data, nil) + _, err = indexedDb2.Put(w.db, channelsStoreName, channelObj) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to put Channel: %+v", err)) + } } // LeaveChannel is called whenever a channel is left locally. func (w *wasmModel) LeaveChannel(channelID *id.ID) { - w.wh.SendMessage(indexedDb.LeaveChannelTag, channelID.Marshal(), nil) + parentErr := errors.New("failed to LeaveChannel") + + // Delete the channel from storage + err := indexedDb2.Delete(w.db, channelsStoreName, + js.ValueOf(channelID.String())) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to delete Channel: %+v", err)) + return + } + + // Clean up lingering data + err = w.deleteMsgByChannel(channelID) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Deleting Channel's Message data failed: %+v", err)) + return + } + jww.DEBUG.Printf("Successfully deleted channel: %s", channelID) +} + +// deleteMsgByChannel is a private helper that uses messageStoreChannelIndex +// to delete all Message with the given Channel ID. +func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error { + parentErr := errors.New("failed to deleteMsgByChannel") + + // Prepare the Transaction + txn, err := w.db.Transaction(idb.TransactionReadWrite, messageStoreName) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(messageStoreName) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to get ObjectStore: %+v", err) + } + index, err := store.Index(messageStoreChannelIndex) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to get Index: %+v", err) + } + + // Perform the operation + channelIdStr := base64.StdEncoding.EncodeToString(channelID.Marshal()) + keyRange, err := idb.NewKeyRangeOnly(js.ValueOf(channelIdStr)) + cursorRequest, err := index.OpenCursorRange(keyRange, idb.CursorNext) + if err != nil { + return errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err) + } + ctx, cancel := indexedDb2.NewContext() + err = cursorRequest.Iter(ctx, + func(cursor *idb.CursorWithValue) error { + _, err := cursor.Delete() + return err + }) + cancel() + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to delete Message data: %+v", err) + } + return nil } // ReceiveMessage is called whenever a message is received on a given channel. @@ -57,58 +148,30 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID, nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 { - msg := channels.ModelMessage{ - Nickname: nickname, - MessageID: messageID, - ChannelID: channelID, - Timestamp: timestamp, - Lease: lease, - Status: status, - Hidden: hidden, - Pinned: false, - Content: []byte(text), - Type: mType, - Round: round.ID, - PubKey: pubKey, - CodesetVersion: codeset, - DmToken: dmToken, - } - - data, err := json.Marshal(msg) - if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal payload for ReceiveMessage: %+v", err) - return 0 - } + textBytes := []byte(text) + var err error - uuidChan := make(chan uint64) - w.wh.SendMessage(indexedDb.ReceiveMessageTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) + // Handle encryption, if it is present + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt([]byte(text)) if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveMessage: %+v", err) - uuidChan <- 0 + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(indexedDb.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveMessage", indexedDb.ResponseTimeout) } - return 0 -} + msgToInsert := buildMessage( + channelID.Marshal(), messageID.Bytes(), nil, nickname, + textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, + false, hidden, status) + + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) + } -// ReceiveReplyMessage is JSON marshalled and sent to the worker for -// [wasmModel.ReceiveReply]. -type ReceiveReplyMessage struct { - ReactionTo message.ID `json:"replyTo"` - channels.ModelMessage `json:"message"` + go w.receivedMessageCB(uuid, channelID, false) + return uuid } // ReceiveReply is called whenever a message is received that is a reply on a @@ -122,54 +185,29 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 { - msg := ReceiveReplyMessage{ - ReactionTo: replyTo, - ModelMessage: channels.ModelMessage{ - Nickname: nickname, - MessageID: messageID, - ChannelID: channelID, - Timestamp: timestamp, - Lease: lease, - Status: status, - Hidden: hidden, - Pinned: false, - Content: []byte(text), - Type: mType, - Round: round.ID, - PubKey: pubKey, - CodesetVersion: codeset, - DmToken: dmToken, - }, - } - - data, err := json.Marshal(msg) - if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal payload for ReceiveReply: %+v", err) - return 0 - } + textBytes := []byte(text) + var err error - uuidChan := make(chan uint64) - w.wh.SendMessage(indexedDb.ReceiveReplyTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) + // Handle encryption, if it is present + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt([]byte(text)) if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveReply: %+v", err) - uuidChan <- 0 + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(indexedDb.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveReply", indexedDb.ResponseTimeout) } - return 0 + msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(), + replyTo.Bytes(), nickname, textBytes, pubKey, dmToken, codeset, + timestamp, lease, round.ID, mType, hidden, false, status) + + uuid, err := w.receiveHelper(msgToInsert, false) + + if err != nil { + jww.ERROR.Printf("Failed to receive reply: %+v", err) + } + go w.receivedMessageCB(uuid, channelID, false) + return uuid } // ReceiveReaction is called whenever a reaction to a message is received on a @@ -183,79 +221,29 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 { + textBytes := []byte(reaction) + var err error - msg := ReceiveReplyMessage{ - ReactionTo: reactionTo, - ModelMessage: channels.ModelMessage{ - Nickname: nickname, - MessageID: messageID, - ChannelID: channelID, - Timestamp: timestamp, - Lease: lease, - Status: status, - Hidden: hidden, - Pinned: false, - Content: []byte(reaction), - Type: mType, - Round: round.ID, - PubKey: pubKey, - CodesetVersion: codeset, - DmToken: dmToken, - }, - } - - data, err := json.Marshal(msg) - if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal payload for ReceiveReaction: %+v", err) - return 0 - } - - uuidChan := make(chan uint64) - w.wh.SendMessage(indexedDb.ReceiveReactionTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) + // Handle encryption, if it is present + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt([]byte(reaction)) if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveReaction: %+v", err) - uuidChan <- 0 + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(indexedDb.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveReply", indexedDb.ResponseTimeout) } - return 0 -} - -// MessageUpdateInfo is JSON marshalled and sent to the worker for -// [wasmModel.UpdateFromMessageID] and [wasmModel.UpdateFromUUID]. -type MessageUpdateInfo struct { - UUID uint64 `json:"uuid"` - - MessageID message.ID `json:"messageID"` - MessageIDSet bool `json:"messageIDSet"` - - Timestamp time.Time `json:"timestamp"` - TimestampSet bool `json:"timestampSet"` + msgToInsert := buildMessage( + channelID.Marshal(), messageID.Bytes(), reactionTo.Bytes(), nickname, + textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, + false, hidden, status) - RoundID id.Round `json:"round"` - RoundIDSet bool `json:"roundIDSet"` - - Pinned bool `json:"pinned"` - PinnedSet bool `json:"pinnedSet"` - - Hidden bool `json:"hidden"` - HiddenSet bool `json:"hiddenSet"` - - Status channels.SentStatus `json:"status"` - StatusSet bool `json:"statusSet"` + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive reaction: %+v", err) + } + go w.receivedMessageCB(uuid, channelID, false) + return uuid } // UpdateFromUUID is called whenever a message at the UUID is modified. @@ -266,40 +254,31 @@ type MessageUpdateInfo struct { func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, status *channels.SentStatus) { - msg := MessageUpdateInfo{UUID: uuid} - if messageID != nil { - msg.MessageID = *messageID - msg.MessageIDSet = true - } - if timestamp != nil { - msg.Timestamp = *timestamp - msg.TimestampSet = true - } - if round != nil { - msg.RoundID = round.ID - msg.RoundIDSet = true - } - if pinned != nil { - msg.Pinned = *pinned - msg.PinnedSet = true - } - if hidden != nil { - msg.Hidden = *hidden - msg.HiddenSet = true - } - if status != nil { - msg.Status = *status - msg.StatusSet = true - } + parentErr := errors.New("failed to UpdateFromUUID") - data, err := json.Marshal(msg) + // FIXME: this is a bit of race condition without the mux. + // This should be done via the transactions (i.e., make a + // special version of receiveHelper) + w.updateMux.Lock() + defer w.updateMux.Unlock() + + // Convert messageID to the key generated by json.Marshal + key := js.ValueOf(uuid) + + // Use the key to get the existing Message + currentMsg, err := indexedDb2.Get(w.db, messageStoreName, key) if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal payload for UpdateFromUUID: %+v", err) + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get message: %+v", err)) return } - w.wh.SendMessage(indexedDb.UpdateFromUUIDTag, data, nil) + _, err = w.updateMessage(utils.JsToJson(currentMsg), messageID, timestamp, + round, pinned, hidden, status) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to updateMessage: %+v", err)) + } } // UpdateFromMessageID is called whenever a message with the message ID is @@ -314,109 +293,222 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, func (w *wasmModel) UpdateFromMessageID(messageID message.ID, timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, status *channels.SentStatus) uint64 { + parentErr := errors.New("failed to UpdateFromMessageID") - msg := MessageUpdateInfo{MessageID: messageID, MessageIDSet: true} - if timestamp != nil { - msg.Timestamp = *timestamp - msg.TimestampSet = true + // FIXME: this is a bit of race condition without the mux. + // This should be done via the transactions (i.e., make a + // special version of receiveHelper) + w.updateMux.Lock() + defer w.updateMux.Unlock() + + msgIDStr := base64.StdEncoding.EncodeToString(messageID.Marshal()) + currentMsgObj, err := indexedDb2.GetIndex(w.db, messageStoreName, + messageStoreMessageIndex, js.ValueOf(msgIDStr)) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Failed to get message by index: %+v", err)) + return 0 } + + currentMsg := utils.JsToJson(currentMsgObj) + uuid, err := w.updateMessage(currentMsg, &messageID, timestamp, + round, pinned, hidden, status) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to updateMessage: %+v", err)) + } + return uuid +} + +// updateMessage is a helper for updating a stored message. +func (w *wasmModel) updateMessage(currentMsgJson string, messageID *message.ID, + timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, + status *channels.SentStatus) (uint64, error) { + + newMessage := &Message{} + err := json.Unmarshal([]byte(currentMsgJson), newMessage) + if err != nil { + return 0, err + } + + if status != nil { + newMessage.Status = uint8(*status) + } + if messageID != nil { + newMessage.MessageID = messageID.Bytes() + } + if round != nil { - msg.RoundID = round.ID - msg.RoundIDSet = true + newMessage.Round = uint64(round.ID) + } + + if timestamp != nil { + newMessage.Timestamp = *timestamp } + if pinned != nil { - msg.Pinned = *pinned - msg.PinnedSet = true + newMessage.Pinned = *pinned } + if hidden != nil { - msg.Hidden = *hidden - msg.HiddenSet = true + newMessage.Hidden = *hidden } - if status != nil { - msg.Status = *status - msg.StatusSet = true + + // Store the updated Message + uuid, err := w.receiveHelper(newMessage, true) + if err != nil { + return 0, err + } + channelID := &id.ID{} + copy(channelID[:], newMessage.ChannelID) + go w.receivedMessageCB(uuid, channelID, true) + + return uuid, nil +} + +// buildMessage is a private helper that converts typical [channels.EventModel] +// inputs into a basic Message structure for insertion into storage. +// +// NOTE: ID is not set inside this function because we want to use the +// autoincrement key by default. If you are trying to overwrite an existing +// message, then you need to set it manually yourself. +func buildMessage(channelID, messageID, parentID []byte, nickname string, + text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, + timestamp time.Time, lease time.Duration, round id.Round, + mType channels.MessageType, pinned, hidden bool, + status channels.SentStatus) *Message { + return &Message{ + MessageID: messageID, + Nickname: nickname, + ChannelID: channelID, + ParentMessageID: parentID, + Timestamp: timestamp, + Lease: lease, + Status: uint8(status), + Hidden: hidden, + Pinned: pinned, + Text: text, + Type: uint16(mType), + Round: uint64(round), + // User Identity Info + Pubkey: pubKey, + DmToken: dmToken, + CodesetVersion: codeset, } +} - data, err := json.Marshal(msg) +// receiveHelper is a private helper for receiving any sort of message. +func (w *wasmModel) receiveHelper( + newMessage *Message, isUpdate bool) (uint64, error) { + // Convert to jsObject + newMessageJson, err := json.Marshal(newMessage) if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal payload for UpdateFromMessageID: %+v", err) - return 0 + return 0, errors.Errorf("Unable to marshal Message: %+v", err) + } + messageObj, err := utils.JsonToJS(newMessageJson) + if err != nil { + return 0, errors.Errorf("Unable to marshal Message: %+v", err) } - uuidChan := make(chan uint64) - w.wh.SendMessage(indexedDb.UpdateFromMessageIDTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to UpdateFromMessageID: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) + // Unset the primaryKey for inserts so that it can be auto-populated and + // incremented + if !isUpdate { + messageObj.Delete("id") + } - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(indexedDb.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveReply", indexedDb.ResponseTimeout) + // Store message to database + result, err := indexedDb2.Put(w.db, messageStoreName, messageObj) + if err != nil && !strings.Contains(err.Error(), + "at least one key does not satisfy the uniqueness requirements") { + // Only return non-unique constraint errors so that the case + // below this one can be hit and handle duplicate entries properly. + return 0, errors.Errorf("Unable to put Message: %+v", err) } - return 0 -} + // NOTE: Sometimes the insert fails to return an error but hits a duplicate + // insert, so this fallthrough returns the UUID entry in that case. + if result.IsUndefined() { + msgID := message.ID{} + copy(msgID[:], newMessage.MessageID) + msg, errLookup := w.msgIDLookup(msgID) + if errLookup == nil && msg.ID != 0 { + return msg.ID, nil + } + return 0, errors.Errorf("uuid lookup failure: %+v", err) + } + uuid := uint64(result.Int()) + jww.DEBUG.Printf("Successfully stored message %d", uuid) -// GetMessageMessage is JSON marshalled and sent to the worker for -// [wasmModel.GetMessage]. -type GetMessageMessage struct { - Message channels.ModelMessage `json:"message"` - Error string `json:"error"` + return uuid, nil } // GetMessage returns the message with the given [channel.MessageID]. func (w *wasmModel) GetMessage( messageID message.ID) (channels.ModelMessage, error) { - msgChan := make(chan GetMessageMessage) - w.wh.SendMessage(indexedDb.GetMessageTag, messageID.Marshal(), func(data []byte) { - var msg GetMessageMessage - err := json.Unmarshal(data, &msg) + lookupResult, err := w.msgIDLookup(messageID) + if err != nil { + return channels.ModelMessage{}, err + } + + var channelId *id.ID + if lookupResult.ChannelID != nil { + channelId, err = id.Unmarshal(lookupResult.ChannelID) if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to GetMessage: %+v", err) + return channels.ModelMessage{}, err } - msgChan <- msg - }) + } - select { - case msg := <-msgChan: - if msg.Error != "" { - return channels.ModelMessage{}, errors.New(msg.Error) + var parentMsgId message.ID + if lookupResult.ParentMessageID != nil { + parentMsgId, err = message.UnmarshalID(lookupResult.ParentMessageID) + if err != nil { + return channels.ModelMessage{}, err } - return msg.Message, nil - case <-time.After(indexedDb.ResponseTimeout): - return channels.ModelMessage{}, errors.Errorf("timed out after %s "+ - "waiting for response from the worker about GetMessage", - indexedDb.ResponseTimeout) } + + return channels.ModelMessage{ + UUID: lookupResult.ID, + Nickname: lookupResult.Nickname, + MessageID: messageID, + ChannelID: channelId, + ParentMessageID: parentMsgId, + Timestamp: lookupResult.Timestamp, + Lease: lookupResult.Lease, + Status: channels.SentStatus(lookupResult.Status), + Hidden: lookupResult.Hidden, + Pinned: lookupResult.Pinned, + Content: lookupResult.Text, + Type: channels.MessageType(lookupResult.Type), + Round: id.Round(lookupResult.Round), + PubKey: lookupResult.Pubkey, + CodesetVersion: lookupResult.CodesetVersion, + }, nil } // DeleteMessage removes a message with the given messageID from storage. func (w *wasmModel) DeleteMessage(messageID message.ID) error { - errChan := make(chan error) - w.wh.SendMessage(indexedDb.DeleteMessageTag, messageID.Marshal(), func(data []byte) { - if data != nil { - errChan <- errors.New(string(data)) - } else { - errChan <- nil - } - }) + msgId := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes())) + return indexedDb2.DeleteIndex(w.db, messageStoreName, + messageStoreMessageIndex, pkeyName, msgId) +} + +// msgIDLookup gets the UUID of the Message with the given messageID. +func (w *wasmModel) msgIDLookup(messageID message.ID) (*Message, error) { + msgIDStr := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes())) + resultObj, err := indexedDb2.GetIndex(w.db, messageStoreName, + messageStoreMessageIndex, msgIDStr) + if err != nil { + return nil, err + } else if resultObj.IsUndefined() { + return nil, errors.Errorf("no message for %s found", msgIDStr) + } - select { - case err := <-errChan: - return err - case <-time.After(indexedDb.ResponseTimeout): - return errors.Errorf("timed out after %s waiting for response from "+ - "the worker about DeleteMessage", indexedDb.ResponseTimeout) + // Process result into string + resultMsg := &Message{} + err = json.Unmarshal([]byte(utils.JsToJson(resultObj)), resultMsg) + if err != nil { + return nil, err } + return resultMsg, nil + } diff --git a/indexedDbWorker/channels/implementation_test.go b/indexedDb/channels/implementation_test.go similarity index 95% rename from indexedDbWorker/channels/implementation_test.go rename to indexedDb/channels/implementation_test.go index 1a78e4702848e71d5b50b132dfacf018836360f9..22e59146634d68fadf9c6b808bb0cd406eb38caa 100644 --- a/indexedDbWorker/channels/implementation_test.go +++ b/indexedDb/channels/implementation_test.go @@ -14,7 +14,7 @@ import ( "fmt" "github.com/hack-pad/go-indexeddb/idb" "gitlab.com/elixxir/crypto/message" - "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/primitives/netTime" @@ -107,7 +107,7 @@ func TestWasmModel_DeleteMessage(t *testing.T) { } // Check the resulting status - results, err := indexedDbWorker.Dump(eventModel.db, messageStoreName) + results, err := indexedDb2.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -122,7 +122,7 @@ func TestWasmModel_DeleteMessage(t *testing.T) { } // Check the resulting status - results, err = indexedDbWorker.Dump(eventModel.db, messageStoreName) + results, err = indexedDb2.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -164,7 +164,7 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { } // Ensure one message is stored - results, err := indexedDbWorker.Dump(eventModel.db, messageStoreName) + results, err := indexedDb2.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -178,7 +178,7 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { uuid, nil, nil, nil, nil, nil, &expectedStatus) // Check the resulting status - results, err = indexedDbWorker.Dump(eventModel.db, messageStoreName) + results, err = indexedDb2.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -236,7 +236,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { } eventModel.JoinChannel(testChannel) eventModel.JoinChannel(testChannel2) - results, err := indexedDbWorker.Dump(eventModel.db, channelsStoreName) + results, err := indexedDb2.Dump(eventModel.db, channelsStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -244,7 +244,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { t.Fatalf("Expected 2 channels to exist") } eventModel.LeaveChannel(testChannel.ReceptionID) - results, err = indexedDbWorker.Dump(eventModel.db, channelsStoreName) + results, err = indexedDb2.Dump(eventModel.db, channelsStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -396,7 +396,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { } // Check pre-results - result, err := indexedDbWorker.Dump(eventModel.db, messageStoreName) + result, err := indexedDb2.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -411,7 +411,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { } // Check final results - result, err = indexedDbWorker.Dump(eventModel.db, messageStoreName) + result, err = indexedDb2.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } diff --git a/indexedDb/channels/init.go b/indexedDb/channels/init.go index 20f5108def2a3f88e200a1841036836d19d81e67..b9f63e391bbe9409312922ed9a8a621e7d0577db 100644 --- a/indexedDb/channels/init.go +++ b/indexedDb/channels/init.go @@ -7,168 +7,248 @@ //go:build js && wasm -package channels +package main import ( - "encoding/json" + "github.com/hack-pad/go-indexeddb/idb" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/channels" cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" "gitlab.com/elixxir/xxdk-wasm/indexedDb" - "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/xx_network/primitives/id" + "syscall/js" "time" ) -// WorkerJavascriptFileURL is the URL of the script the worker will execute to -// launch the worker WASM binary. It must obey the same-origin policy. -const WorkerJavascriptFileURL = "/integrations/assets/dmIndexedDbWorker.js" +const ( + // databaseSuffix is the suffix to be appended to the name of + // the database. + databaseSuffix = "_speakeasy" + + // currentVersion is the current version of the IndexDb + // runtime. Used for migration purposes. + currentVersion uint = 1 +) // MessageReceivedCallback is called any time a message is received or updated. // // update is true if the row is old and was edited. type MessageReceivedCallback func(uuid uint64, channelID *id.ID, update bool) -// NewWASMEventModelBuilder returns an EventModelBuilder which allows -// the channel manager to define the path but the callback is the same -// across the board. -func NewWASMEventModelBuilder(encryption cryptoChannel.Cipher, - cb MessageReceivedCallback) channels.EventModelBuilder { - fn := func(path string) (channels.EventModel, error) { - return NewWASMEventModel(path, encryption, cb) - } - return fn -} - -// NewWASMEventModelMessage is JSON marshalled and sent to the worker for -// [NewWASMEventModel]. -type NewWASMEventModelMessage struct { - Path string `json:"path"` - EncryptionJSON string `json:"encryptionJSON"` -} - // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel. // The name should be a base64 encoding of the users public key. func NewWASMEventModel(path string, encryption cryptoChannel.Cipher, - cb MessageReceivedCallback) (channels.EventModel, error) { + cb MessageReceivedCallback, storeEncryptionStatus storeEncryptionStatusFn) ( + channels.EventModel, error) { + databaseName := path + databaseSuffix + return newWASMModel(databaseName, encryption, cb, storeEncryptionStatus) +} - // TODO: bring in URL and name from caller - wh, err := indexedDb.NewWorkerHandler( - WorkerJavascriptFileURL, "channelsIndexedDb") +// storeEncryptionStatusFn matches storage.StoreIndexedDbEncryptionStatus so +// that the data can be sent between the worker and main thread. +type storeEncryptionStatusFn func( + databaseName string, encryptionStatus bool) (bool, error) + +// newWASMModel creates the given [idb.Database] and returns a wasmModel. +func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, + cb MessageReceivedCallback, storeEncryptionStatus storeEncryptionStatusFn) ( + *wasmModel, error) { + // Attempt to open database object + ctx, cancel := indexedDb2.NewContext() + defer cancel() + openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion, + func(db *idb.Database, oldVersion, newVersion uint) error { + if oldVersion == newVersion { + jww.INFO.Printf("IndexDb version is current: v%d", + newVersion) + return nil + } + + jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d", + oldVersion, newVersion) + + if oldVersion == 0 && newVersion >= 1 { + err := v1Upgrade(db) + if err != nil { + return err + } + oldVersion = 1 + } + + // if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 } + return nil + }) if err != nil { return nil, err } - // Register handler to manage messages for the MessageReceivedCallback - wh.RegisterHandler(indexedDb.GetMessageTag, indexedDb.InitID, false, - messageReceivedCallbackHandler(cb)) - - // Register handler to manage checking encryption status from local storage - wh.RegisterHandler(indexedDb.EncryptionStatusTag, indexedDb.InitID, false, - checkDbEncryptionStatusHandler(wh)) + // Wait for database open to finish + db, err := openRequest.Await(ctx) + if err != nil { + return nil, err + } - encryptionJSON, err := json.Marshal(encryption) + // Save the encryption status to storage + encryptionStatus := encryption != nil + loadedEncryptionStatus, err := + storeEncryptionStatus(databaseName, encryptionStatus) if err != nil { return nil, err } - message := NewWASMEventModelMessage{ - Path: path, - EncryptionJSON: string(encryptionJSON), + // Verify encryption status does not change + if encryptionStatus != loadedEncryptionStatus { + return nil, errors.New( + "Cannot load database with different encryption status.") + } else if !encryptionStatus { + jww.WARN.Printf("IndexedDb encryption disabled!") } - payload, err := json.Marshal(message) + // Attempt to ensure the database has been properly initialized + openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion, + func(db *idb.Database, oldVersion, newVersion uint) error { + return nil + }) if err != nil { return nil, err } + // Wait for database open to finish + db, err = openRequest.Await(ctx) + if err != nil { + return nil, err + } + wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption} - errChan := make(chan string) - wh.SendMessage(indexedDb.NewWASMEventModelTag, payload, func(data []byte) { - errChan <- string(data) - }) + return wrapper, nil +} - select { - case workerErr := <-errChan: - if workerErr != "" { - return nil, errors.New(workerErr) - } - case <-time.After(indexedDb.ResponseTimeout): - return nil, errors.Errorf("timed out after %s waiting for indexedDB "+ - "database in worker to intialize", indexedDb.ResponseTimeout) +// v1Upgrade performs the v0 -> v1 database upgrade. +// +// This can never be changed without permanently breaking backwards +// compatibility. +func v1Upgrade(db *idb.Database) error { + storeOpts := idb.ObjectStoreOptions{ + KeyPath: js.ValueOf(pkeyName), + AutoIncrement: true, + } + indexOpts := idb.IndexOptions{ + Unique: false, + MultiEntry: false, } - return &wasmModel{wh}, nil -} + // Build Message ObjectStore and Indexes + messageStore, err := db.CreateObjectStore(messageStoreName, storeOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreMessageIndex, + js.ValueOf(messageStoreMessage), + idb.IndexOptions{ + Unique: true, + MultiEntry: false, + }) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreChannelIndex, + js.ValueOf(messageStoreChannel), indexOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreParentIndex, + js.ValueOf(messageStoreParent), indexOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreTimestampIndex, + js.ValueOf(messageStoreTimestamp), indexOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStorePinnedIndex, + js.ValueOf(messageStorePinned), indexOpts) + if err != nil { + return err + } -// MessageReceivedCallbackMessage is JSON marshalled and received from the -// worker for the [MessageReceivedCallback] callback. -type MessageReceivedCallbackMessage struct { - UUID uint64 `json:"uuid"` - ChannelID *id.ID `json:"channelID"` - Update bool `json:"update"` -} + // Build Channel ObjectStore + _, err = db.CreateObjectStore(channelsStoreName, storeOpts) + if err != nil { + return err + } -// messageReceivedCallbackHandler returns a handler to manage messages for the -// MessageReceivedCallback. -func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte) { - return func(data []byte) { - var msg MessageReceivedCallbackMessage - err := json.Unmarshal(data, &msg) - if err != nil { - jww.ERROR.Printf("Failed to JSON unmarshal "+ - "MessageReceivedCallback message from worker: %+v", err) - return - } - cb(msg.UUID, msg.ChannelID, msg.Update) + // Get the database name and save it to storage + if databaseName, err2 := db.Name(); err2 != nil { + return err2 + } else if err = storeDatabaseName(databaseName); err != nil { + return err } -} -// EncryptionStatusMessage is JSON marshalled and received from the worker when -// the database checks if it is encrypted. -type EncryptionStatusMessage struct { - DatabaseName string `json:"databaseName"` - EncryptionStatus bool `json:"encryptionStatus"` + return nil } -// EncryptionStatusReply is JSON marshalled and sent to the worker is response -// to the [EncryptionStatusMessage]. -type EncryptionStatusReply struct { - EncryptionStatus bool `json:"encryptionStatus"` - Error string `json:"error"` +// hackTestDb is a horrible function that exists as the result of an extremely +// long discussion about why initializing the IndexedDb sometimes silently +// fails. It ultimately tries to prevent an unrecoverable situation by actually +// inserting some nonsense data and then checking to see if it persists. +// If this function still exists in 2023, god help us all. Amen. +func (w *wasmModel) hackTestDb() error { + testMessage := &Message{ + ID: 0, + Nickname: "test", + MessageID: id.DummyUser.Marshal(), + } + msgId, helper := w.receiveHelper(testMessage, false) + if helper != nil { + return helper + } + result, err := indexedDb2.Get(w.db, messageStoreName, js.ValueOf(msgId)) + if err != nil { + return err + } + if result.IsUndefined() { + return errors.Errorf("Failed to test db, record not present") + } + return nil } -// checkDbEncryptionStatusHandler returns a handler to manage checking -// encryption status from local storage. -func checkDbEncryptionStatusHandler(wh *indexedDb.WorkerHandler) func(data []byte) { - return func(data []byte) { - // Unmarshal received message - var msg EncryptionStatusMessage - err := json.Unmarshal(data, &msg) - if err != nil { - jww.ERROR.Printf("Failed to JSON unmarshal "+ - "EncryptionStatusMessage message from worker: %+v", err) - return - } - - // Pass message values to storage - loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( - msg.DatabaseName, msg.EncryptionStatus) - var reply EncryptionStatusReply - if err != nil { - reply.Error = err.Error() - } else { - reply.EncryptionStatus = loadedEncryptionStatus - } +// storeDatabaseName sends the database name to storage.StoreIndexedDb in the +// main thread to be stored in localstorage and waits for the error to be +// returned. +// +// The function specified below is a placeholder until set by +// registerDatabaseNameStore. registerDatabaseNameStore must be called before +// storeDatabaseName. +var storeDatabaseName = func(databaseName string) error { return nil } + +// RegisterDatabaseNameStore sets storeDatabaseName to send the database to +// storage.StoreIndexedDb in the main thread when called and registers a handler +// to listen for the response. +func RegisterDatabaseNameStore(m *manager) { + storeDatabaseNameResponseChan := make(chan []byte) + // Register handler + m.mh.RegisterHandler(indexedDb.StoreDatabaseNameTag, func(data []byte) []byte { + storeDatabaseNameResponseChan <- data + return nil + }) - // Return response - statusData, err := json.Marshal(reply) - if err != nil { - jww.ERROR.Printf( - "Failed to JSON marshal EncryptionStatusReply: %+v", err) - return + storeDatabaseName = func(databaseName string) error { + m.mh.SendResponse(indexedDb.StoreDatabaseNameTag, indexedDb.InitID, + []byte(databaseName)) + + // Wait for response + select { + case response := <-storeDatabaseNameResponseChan: + if len(response) > 0 { + return errors.New(string(response)) + } + case <-time.After(indexedDb.ResponseTimeout): + return errors.Errorf("timed out after %s waiting for "+ + "response about storing the database name in local "+ + "storage in the main thread", indexedDb.ResponseTimeout) } - - wh.SendMessage(indexedDb.EncryptionStatusTag, statusData, nil) + return nil } } diff --git a/indexedDbWorker/channels/main.go b/indexedDb/channels/main.go similarity index 87% rename from indexedDbWorker/channels/main.go rename to indexedDb/channels/main.go index d720ddee52b525d98d1f6027927dd8b750182054..ebe19df08514fc82ebb0543253b51e78435887ab 100644 --- a/indexedDbWorker/channels/main.go +++ b/indexedDb/channels/main.go @@ -11,15 +11,15 @@ package main import ( "fmt" - "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" ) func main() { fmt.Println("Starting xxDK WebAssembly Database Worker.") - m := &manager{mh: indexedDbWorker.NewMessageHandler()} - m.RegisterHandlers() + m := &manager{mh: indexedDb2.NewMessageHandler()} RegisterDatabaseNameStore(m) + m.RegisterHandlers() m.mh.SignalReady() <-make(chan bool) } diff --git a/indexedDbWorker/channels/model.go b/indexedDb/channels/model.go similarity index 100% rename from indexedDbWorker/channels/model.go rename to indexedDb/channels/model.go diff --git a/indexedDbWorker/dm/dmIndexedDbWorker.js b/indexedDb/dm/dmIndexedDbWorker.js similarity index 94% rename from indexedDbWorker/dm/dmIndexedDbWorker.js rename to indexedDb/dm/dmIndexedDbWorker.js index 7c03a409a1b5e62aab21a78dbffa7affa87b53b3..38cd0b5970765048a643b683c6a83d3c670f0888 100644 --- a/indexedDbWorker/dm/dmIndexedDbWorker.js +++ b/indexedDb/dm/dmIndexedDbWorker.js @@ -8,7 +8,7 @@ importScripts('wasm_exec.js'); const go = new Go(); -const binPath = 'xxdk-indexedDkWorker.wasm' +const binPath = 'dmIndexedDbWorker.wasm' WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => { go.run(result.instance); }).catch((err) => { diff --git a/indexedDbWorker/dm/handlers.go b/indexedDb/dm/handlers.go similarity index 98% rename from indexedDbWorker/dm/handlers.go rename to indexedDb/dm/handlers.go index 52bf98e549b7d3fe0233f5096c27115884566d13..9dbec0990fd1b59bfe0dc0405a323992495c6c0f 100644 --- a/indexedDbWorker/dm/handlers.go +++ b/indexedDb/dm/handlers.go @@ -17,10 +17,10 @@ import ( "gitlab.com/elixxir/client/v4/dm" cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/fastRNG" - "gitlab.com/elixxir/xxdk-wasm/indexedDb" - mChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/channels" - mDm "gitlab.com/elixxir/xxdk-wasm/indexedDb/dm" "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" + mChannels "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker/channels" + mDm "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker/dm" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" "gitlab.com/xx_network/crypto/csprng" "time" ) @@ -28,7 +28,7 @@ import ( // manager handles the event model and the message handler, which is used to // send information between the event model and the main thread. type manager struct { - mh *indexedDbWorker.MessageHandler + mh *indexedDb2.MessageHandler model dm.EventModel } diff --git a/indexedDb/dm/implementation.go b/indexedDb/dm/implementation.go index 77b5fbc73d5ec3bea746597694342b522bd382de..7cd2f00e0bad630e3f331a1a6f66657cd6cdb77c 100644 --- a/indexedDb/dm/implementation.go +++ b/indexedDb/dm/implementation.go @@ -7,240 +7,375 @@ //go:build js && wasm -package channelEventModel +package main import ( "crypto/ed25519" "encoding/json" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "strings" + "sync" + "syscall/js" "time" + "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/cmix/rounds" "gitlab.com/elixxir/client/v4/dm" + "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/xx_network/primitives/id" + + "github.com/hack-pad/go-indexeddb/idb" + cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/message" - "gitlab.com/elixxir/xxdk-wasm/indexedDb" ) // wasmModel implements dm.EventModel interface, which uses the channels system // passed an object that adheres to in order to get events on the channel. type wasmModel struct { - wh *indexedDb.WorkerHandler + db *idb.Database + cipher cryptoChannel.Cipher + receivedMessageCB MessageReceivedCallback + updateMux sync.Mutex +} + +// joinConversation is used for joining new conversations. +func (w *wasmModel) joinConversation(nickname string, + pubKey ed25519.PublicKey, dmToken uint32, codeset uint8) error { + parentErr := errors.New("failed to joinConversation") + + // Build object + newConvo := Conversation{ + Pubkey: pubKey, + Nickname: nickname, + Token: dmToken, + CodesetVersion: codeset, + Blocked: false, + } + + // Convert to jsObject + newConvoJson, err := json.Marshal(&newConvo) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to marshal Conversation: %+v", err) + } + convoObj, err := utils.JsonToJS(newConvoJson) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to marshal Conversation: %+v", err) + } + + _, err = indexedDb2.Put(w.db, conversationStoreName, convoObj) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to put Conversation: %+v", err) + } + return nil } -// TransferMessage is JSON marshalled and sent to the worker. -type TransferMessage struct { - UUID uint64 `json:"uuid"` - MessageID message.ID `json:"messageID"` - ReactionTo message.ID `json:"reactionTo"` - Nickname string `json:"nickname"` - Text []byte `json:"text"` - PubKey ed25519.PublicKey `json:"pubKey"` - DmToken uint32 `json:"dmToken"` - Codeset uint8 `json:"codeset"` - Timestamp time.Time `json:"timestamp"` - Round rounds.Round `json:"round"` - MType dm.MessageType `json:"mType"` - Status dm.Status `json:"status"` +// buildMessage is a private helper that converts typical dm.EventModel inputs +// into a basic Message structure for insertion into storage. +// +// NOTE: ID is not set inside this function because we want to use the +// autoincrement key by default. If you are trying to overwrite an existing +// message, then you need to set it manually yourself. +func buildMessage(messageID, parentID, text []byte, pubKey ed25519.PublicKey, + timestamp time.Time, round id.Round, mType dm.MessageType, + status dm.Status) *Message { + return &Message{ + MessageID: messageID, + ConversationPubKey: pubKey, + ParentMessageID: parentID, + Timestamp: timestamp, + Status: uint8(status), + Text: text, + Type: uint16(mType), + Round: uint64(round), + } } func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, round rounds.Round, mType dm.MessageType, status dm.Status) uint64 { - msg := TransferMessage{ - MessageID: messageID, - Nickname: nickname, - Text: text, - PubKey: pubKey, - DmToken: dmToken, - Codeset: codeset, - Timestamp: timestamp, - Round: round, - MType: mType, - Status: status, - } - - data, err := json.Marshal(msg) - if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal payload for TransferMessage: %+v", err) + parentErr := errors.New("failed to Receive") + + // If there is no extant Conversation, create one. + _, err := indexedDb2.Get( + w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) + if err != nil { + if strings.Contains(err.Error(), indexedDb2.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) } - uuidChan := make(chan uint64) - w.wh.SendMessage(indexedDb.ReceiveTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) + // Handle encryption, if it is present + if w.cipher != nil { + text, err = w.cipher.Encrypt(text) if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to Receive: %+v", err) - uuidChan <- 0 + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 } - uuidChan <- uuid - }) + } - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(indexedDb.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about Receive", indexedDb.ResponseTimeout) + msgToInsert := buildMessage(messageID.Bytes(), nil, text, pubKey, timestamp, + round.ID, mType, status) + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) } - return 0 + go w.receivedMessageCB(uuid, pubKey, false) + return uuid } func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, round rounds.Round, status dm.Status) uint64 { - msg := TransferMessage{ - MessageID: messageID, - Nickname: nickname, - Text: []byte(text), - PubKey: pubKey, - DmToken: dmToken, - Codeset: codeset, - Timestamp: timestamp, - Round: round, - Status: status, - } - - data, err := json.Marshal(msg) - if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal payload for TransferMessage: %+v", err) + parentErr := errors.New("failed to ReceiveText") + + // If there is no extant Conversation, create one. + _, err := indexedDb2.Get( + w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) + if err != nil { + if strings.Contains(err.Error(), indexedDb2.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) } - uuidChan := make(chan uint64) - w.wh.SendMessage(indexedDb.ReceiveTextTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) + // Handle encryption, if it is present + textBytes := []byte(text) + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt(textBytes) if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveText: %+v", err) - uuidChan <- 0 + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 } - uuidChan <- uuid - }) + } - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(indexedDb.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveText", indexedDb.ResponseTimeout) + msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes, + pubKey, timestamp, round.ID, dm.TextType, status) + + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) } - return 0 + go w.receivedMessageCB(uuid, pubKey, false) + return uuid } func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, round rounds.Round, status dm.Status) uint64 { - msg := TransferMessage{ - MessageID: messageID, - ReactionTo: reactionTo, - Nickname: nickname, - Text: []byte(text), - PubKey: pubKey, - DmToken: dmToken, - Codeset: codeset, - Timestamp: timestamp, - Round: round, - Status: status, - } - - data, err := json.Marshal(msg) - if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal payload for TransferMessage: %+v", err) + parentErr := errors.New("failed to ReceiveReply") + + // If there is no extant Conversation, create one. + _, err := indexedDb2.Get( + w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) + if err != nil { + if strings.Contains(err.Error(), indexedDb2.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) } - uuidChan := make(chan uint64) - w.wh.SendMessage(indexedDb.ReceiveReplyTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) + // Handle encryption, if it is present + textBytes := []byte(text) + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt(textBytes) if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveReply: %+v", err) - uuidChan <- 0 + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 } - uuidChan <- uuid - }) + } + + msgToInsert := buildMessage(messageID.Bytes(), reactionTo.Marshal(), textBytes, + pubKey, timestamp, round.ID, dm.TextType, status) - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(indexedDb.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveReply", indexedDb.ResponseTimeout) + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) } - return 0 + go w.receivedMessageCB(uuid, pubKey, false) + return uuid } -func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname, +func (w *wasmModel) ReceiveReaction(messageID, _ message.ID, nickname, reaction string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, round rounds.Round, status dm.Status) uint64 { - msg := TransferMessage{ - MessageID: messageID, - ReactionTo: reactionTo, - Nickname: nickname, - Text: []byte(reaction), - PubKey: pubKey, - DmToken: dmToken, - Codeset: codeset, - Timestamp: timestamp, - Round: round, - Status: status, - } - - data, err := json.Marshal(msg) - if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal payload for TransferMessage: %+v", err) + parentErr := errors.New("failed to ReceiveText") + + // If there is no extant Conversation, create one. + _, err := indexedDb2.Get( + w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) + if err != nil { + if strings.Contains(err.Error(), indexedDb2.ErrDoesNotExist) { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + if err != nil { + jww.ERROR.Printf("%+v", err) + } + } else { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + } return 0 + } else { + jww.DEBUG.Printf("Conversation with %s already joined", nickname) } - uuidChan := make(chan uint64) - w.wh.SendMessage(indexedDb.ReceiveReactionTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) + // Handle encryption, if it is present + textBytes := []byte(reaction) + if w.cipher != nil { + textBytes, err = w.cipher.Encrypt(textBytes) if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveReaction: %+v", err) - uuidChan <- 0 + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 } - uuidChan <- uuid - }) + } + + msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes, + pubKey, timestamp, round.ID, dm.ReactionType, status) - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(indexedDb.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveReaction", indexedDb.ResponseTimeout) + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) } - return 0 + go w.receivedMessageCB(uuid, pubKey, false) + return uuid } func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID, timestamp time.Time, round rounds.Round, status dm.Status) { - msg := TransferMessage{ - UUID: uuid, - MessageID: messageID, - Timestamp: timestamp, - Round: round, - Status: status, + parentErr := errors.New("failed to UpdateSentStatus") + + // FIXME: this is a bit of race condition without the mux. + // This should be done via the transactions (i.e., make a + // special version of receiveHelper) + w.updateMux.Lock() + defer w.updateMux.Unlock() + + // Convert messageID to the key generated by json.Marshal + key := js.ValueOf(uuid) + + // Use the key to get the existing Message + currentMsg, err := indexedDb2.Get(w.db, messageStoreName, key) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get message: %+v", err)) + return + } + + // Extract the existing Message and update the Status + newMessage := &Message{} + err = json.Unmarshal([]byte(utils.JsToJson(currentMsg)), newMessage) + if err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Could not JSON unmarshal message: %+v", err)) + return + } + + newMessage.Status = uint8(status) + if !messageID.Equals(message.ID{}) { + newMessage.MessageID = messageID.Bytes() + } + + if round.ID != 0 { + newMessage.Round = uint64(round.ID) + } + + if !timestamp.Equal(time.Time{}) { + newMessage.Timestamp = timestamp + } + + // Store the updated Message + _, err = w.receiveHelper(newMessage, true) + if err != nil { + jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error())) + } + go w.receivedMessageCB(uuid, newMessage.ConversationPubKey, true) +} + +// receiveHelper is a private helper for receiving any sort of message. +func (w *wasmModel) receiveHelper( + newMessage *Message, isUpdate bool) (uint64, error) { + // Convert to jsObject + newMessageJson, err := json.Marshal(newMessage) + if err != nil { + return 0, errors.Errorf("Unable to marshal Message: %+v", err) + } + messageObj, err := utils.JsonToJS(newMessageJson) + if err != nil { + return 0, errors.Errorf("Unable to marshal Message: %+v", err) + } + + // Unset the primaryKey for inserts so that it can be auto-populated and + // incremented + if !isUpdate { + messageObj.Delete("id") } - data, err := json.Marshal(msg) + // Store message to database + result, err := indexedDb2.Put(w.db, messageStoreName, messageObj) if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal payload for TransferMessage: %+v", err) + return 0, errors.Errorf("Unable to put Message: %+v", err) } - w.wh.SendMessage(indexedDb.UpdateSentStatusTag, data, nil) + // NOTE: Sometimes the insert fails to return an error but hits a duplicate + // insert, so this fallthrough returns the UUID entry in that case. + if result.IsUndefined() { + msgID := message.ID{} + copy(msgID[:], newMessage.MessageID) + uuid, errLookup := w.msgIDLookup(msgID) + if uuid != 0 && errLookup == nil { + return uuid, nil + } + return 0, errors.Errorf("uuid lookup failure: %+v", err) + } + uuid := uint64(result.Int()) + jww.DEBUG.Printf("Successfully stored message %d", uuid) + + return uuid, nil +} + +// msgIDLookup gets the UUID of the Message with the given messageID. +func (w *wasmModel) msgIDLookup(messageID message.ID) (uint64, error) { + resultObj, err := indexedDb2.GetIndex(w.db, messageStoreName, + messageStoreMessageIndex, utils.CopyBytesToJS(messageID.Marshal())) + if err != nil { + return 0, err + } + + uuid := uint64(0) + if !resultObj.IsUndefined() { + uuid = uint64(resultObj.Get("id").Int()) + } + return uuid, nil } diff --git a/indexedDb/dm/init.go b/indexedDb/dm/init.go index f3bc313ed97dfd0755b36f25a7adc6bd74e746aa..66c9c34a68d8940faf14aa87a1f3b2ba2e020cc9 100644 --- a/indexedDb/dm/init.go +++ b/indexedDb/dm/init.go @@ -7,24 +7,31 @@ //go:build js && wasm -package channelEventModel +package main import ( "crypto/ed25519" - "encoding/json" + "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "syscall/js" "time" + "github.com/hack-pad/go-indexeddb/idb" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/dm" cryptoChannel "gitlab.com/elixxir/crypto/channel" - "gitlab.com/elixxir/xxdk-wasm/indexedDb" - "gitlab.com/elixxir/xxdk-wasm/storage" ) -// WorkerJavascriptFileURL is the URL of the script the worker will execute to -// launch the worker WASM binary. It must obey the same-origin policy. -const WorkerJavascriptFileURL = "/integrations/assets/indexedDbWorker.js" +const ( + // databaseSuffix is the suffix to be appended to the name of + // the database. + databaseSuffix = "_speakeasy_dm" + + // currentVersion is the current version of the IndexDb + // runtime. Used for migration purposes. + currentVersion uint = 1 +) // MessageReceivedCallback is called any time a message is received or updated. // @@ -32,134 +39,192 @@ const WorkerJavascriptFileURL = "/integrations/assets/indexedDbWorker.js" type MessageReceivedCallback func( uuid uint64, pubKey ed25519.PublicKey, update bool) -// NewWASMEventModelMessage is JSON marshalled and sent to the worker for -// [NewWASMEventModel]. -type NewWASMEventModelMessage struct { - Path string `json:"path"` - EncryptionJSON string `json:"encryptionJSON"` -} - // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel. // The name should be a base64 encoding of the users public key. func NewWASMEventModel(path string, encryption cryptoChannel.Cipher, - cb MessageReceivedCallback) (dm.EventModel, error) { + cb MessageReceivedCallback, storeEncryptionStatus storeEncryptionStatusFn) ( + dm.EventModel, error) { + databaseName := path + databaseSuffix + return newWASMModel(databaseName, encryption, cb, storeEncryptionStatus) +} - // TODO: bring in URL and name from caller - wh, err := indexedDb.NewWorkerHandler( - WorkerJavascriptFileURL, "dmIndexedDb") +// storeEncryptionStatusFn matches storage.StoreIndexedDbEncryptionStatus so +// that the data can be sent between the worker and main thread. +type storeEncryptionStatusFn func( + databaseName string, encryptionStatus bool) (bool, error) + +// newWASMModel creates the given [idb.Database] and returns a wasmModel. +func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, + cb MessageReceivedCallback, storeEncryptionStatus storeEncryptionStatusFn) ( + *wasmModel, error) { + // Attempt to open database object + ctx, cancel := indexedDb2.NewContext() + defer cancel() + openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion, + func(db *idb.Database, oldVersion, newVersion uint) error { + if oldVersion == newVersion { + jww.INFO.Printf("IndexDb version is current: v%d", + newVersion) + return nil + } + + jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d", + oldVersion, newVersion) + + if oldVersion == 0 && newVersion >= 1 { + err := v1Upgrade(db) + if err != nil { + return err + } + oldVersion = 1 + } + + // if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 } + return nil + }) if err != nil { return nil, err } - // Register handler to manage messages for the MessageReceivedCallback - wh.RegisterHandler(indexedDb.GetMessageTag, indexedDb.InitID, false, - messageReceivedCallbackHandler(cb)) - - // Register handler to manage checking encryption status from local storage - wh.RegisterHandler(indexedDb.EncryptionStatusTag, indexedDb.InitID, false, - checkDbEncryptionStatusHandler(wh)) + // Wait for database open to finish + db, err := openRequest.Await(ctx) + if err != nil { + return nil, err + } - encryptionJSON, err := json.Marshal(encryption) + // Save the encryption status to storage + encryptionStatus := encryption != nil + loadedEncryptionStatus, err := + storeEncryptionStatus(databaseName, encryptionStatus) if err != nil { return nil, err } - message := NewWASMEventModelMessage{ - Path: path, - EncryptionJSON: string(encryptionJSON), + // Verify encryption status does not change + if encryptionStatus != loadedEncryptionStatus { + return nil, errors.New( + "Cannot load database with different encryption status.") + } else if !encryptionStatus { + jww.WARN.Printf("IndexedDb encryption disabled!") } - payload, err := json.Marshal(message) + // Attempt to ensure the database has been properly initialized + openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion, + func(db *idb.Database, oldVersion, newVersion uint) error { + return nil + }) if err != nil { return nil, err } - - errChan := make(chan string) - wh.SendMessage(indexedDb.NewWASMEventModelTag, payload, func(data []byte) { - errChan <- string(data) - }) - - select { - case workerErr := <-errChan: - if workerErr != "" { - return nil, errors.New(workerErr) - } - case <-time.After(indexedDb.ResponseTimeout): - return nil, errors.Errorf("timed out after %s waiting for indexedDB "+ - "database in worker to intialize", indexedDb.ResponseTimeout) + // Wait for database open to finish + db, err = openRequest.Await(ctx) + if err != nil { + return nil, err } + wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption} - return &wasmModel{wh}, nil + return wrapper, nil } -// MessageReceivedCallbackMessage is JSON marshalled and received from the -// worker for the [MessageReceivedCallback] callback. -type MessageReceivedCallbackMessage struct { - UUID uint64 `json:"uuid"` - PubKey ed25519.PublicKey `json:"pubKey"` - Update bool `json:"update"` -} +// v1Upgrade performs the v0 -> v1 database upgrade. +// +// This can never be changed without permanently breaking backwards +// compatibility. +func v1Upgrade(db *idb.Database) error { + indexOpts := idb.IndexOptions{ + Unique: false, + MultiEntry: false, + } -// messageReceivedCallbackHandler returns a handler to manage messages for the -// MessageReceivedCallback. -func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte) { - return func(data []byte) { - var msg MessageReceivedCallbackMessage - err := json.Unmarshal(data, &msg) - if err != nil { - jww.ERROR.Printf("Failed to JSON unmarshal "+ - "MessageReceivedCallback message from worker: %+v", err) - return - } - cb(msg.UUID, msg.PubKey, msg.Update) + // Build Message ObjectStore and Indexes + messageStoreOpts := idb.ObjectStoreOptions{ + KeyPath: js.ValueOf(msgPkeyName), + AutoIncrement: true, + } + messageStore, err := db.CreateObjectStore(messageStoreName, messageStoreOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreMessageIndex, + js.ValueOf(messageStoreMessage), + idb.IndexOptions{ + Unique: true, + MultiEntry: false, + }) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreConversationIndex, + js.ValueOf(messageStoreConversation), indexOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreParentIndex, + js.ValueOf(messageStoreParent), indexOpts) + if err != nil { + return err + } + _, err = messageStore.CreateIndex(messageStoreTimestampIndex, + js.ValueOf(messageStoreTimestamp), indexOpts) + if err != nil { + return err } -} -// EncryptionStatusMessage is JSON marshalled and received from the worker when -// the database checks if it is encrypted. -type EncryptionStatusMessage struct { - DatabaseName string `json:"databaseName"` - EncryptionStatus bool `json:"encryptionStatus"` -} + // Build Channel ObjectStore + conversationStoreOpts := idb.ObjectStoreOptions{ + KeyPath: js.ValueOf(convoPkeyName), + AutoIncrement: false, + } + _, err = db.CreateObjectStore(conversationStoreName, conversationStoreOpts) + if err != nil { + return err + } -// EncryptionStatusReply is JSON marshalled and sent to the worker is response -// to the [EncryptionStatusMessage]. -type EncryptionStatusReply struct { - EncryptionStatus bool `json:"encryptionStatus"` - Error string `json:"error"` -} + // Get the database name and save it to storage + if databaseName, err2 := db.Name(); err2 != nil { + return err2 + } else if err = storeDatabaseName(databaseName); err != nil { + return err + } -// checkDbEncryptionStatusHandler returns a handler to manage checking -// encryption status from local storage. -func checkDbEncryptionStatusHandler(wh *indexedDb.WorkerHandler) func(data []byte) { - return func(data []byte) { - // Unmarshal received message - var msg EncryptionStatusMessage - err := json.Unmarshal(data, &msg) - if err != nil { - jww.ERROR.Printf("Failed to JSON unmarshal "+ - "EncryptionStatusMessage message from worker: %+v", err) - return - } + return nil +} - // Pass message values to storage - loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( - msg.DatabaseName, msg.EncryptionStatus) - var reply EncryptionStatusReply - if err != nil { - reply.Error = err.Error() - } else { - reply.EncryptionStatus = loadedEncryptionStatus - } +// storeDatabaseName sends the database name to storage.StoreIndexedDb in the +// main thread to be stored in localstorage and waits for the error to be +// returned. +// +// The function specified below is a placeholder until set by +// registerDatabaseNameStore. registerDatabaseNameStore must be called before +// storeDatabaseName. +var storeDatabaseName = func(databaseName string) error { return nil } + +// RegisterDatabaseNameStore sets storeDatabaseName to send the database to +// storage.StoreIndexedDb in the main thread when called and registers a handler +// to listen for the response. +func RegisterDatabaseNameStore(m *manager) { + storeDatabaseNameResponseChan := make(chan []byte) + // Register handler + m.mh.RegisterHandler(indexedDb.StoreDatabaseNameTag, func(data []byte) []byte { + storeDatabaseNameResponseChan <- data + return nil + }) - // Return response - statusData, err := json.Marshal(reply) - if err != nil { - jww.ERROR.Printf( - "Failed to JSON marshal EncryptionStatusReply: %+v", err) - return + storeDatabaseName = func(databaseName string) error { + m.mh.SendResponse(indexedDb.StoreDatabaseNameTag, indexedDb.InitID, + []byte(databaseName)) + + // Wait for response + select { + case response := <-storeDatabaseNameResponseChan: + if len(response) > 0 { + return errors.New(string(response)) + } + case <-time.After(indexedDb.ResponseTimeout): + return errors.Errorf("timed out after %s waiting for "+ + "response about storing the database name in local "+ + "storage in the main thread", indexedDb.ResponseTimeout) } - - wh.SendMessage(indexedDb.EncryptionStatusTag, statusData, nil) + return nil } } diff --git a/indexedDbWorker/dm/main.go b/indexedDb/dm/main.go similarity index 87% rename from indexedDbWorker/dm/main.go rename to indexedDb/dm/main.go index d720ddee52b525d98d1f6027927dd8b750182054..5f0d16dc868a7f8fd1bf67158ec3b9f8e5ffbd90 100644 --- a/indexedDbWorker/dm/main.go +++ b/indexedDb/dm/main.go @@ -11,13 +11,13 @@ package main import ( "fmt" - "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" ) func main() { fmt.Println("Starting xxDK WebAssembly Database Worker.") - m := &manager{mh: indexedDbWorker.NewMessageHandler()} + m := &manager{mh: indexedDb2.NewMessageHandler()} m.RegisterHandlers() RegisterDatabaseNameStore(m) m.mh.SignalReady() diff --git a/indexedDbWorker/dm/model.go b/indexedDb/dm/model.go similarity index 100% rename from indexedDbWorker/dm/model.go rename to indexedDb/dm/model.go diff --git a/indexedDbWorker/messageHandler.go b/indexedDb/messageHandler.go similarity index 98% rename from indexedDbWorker/messageHandler.go rename to indexedDb/messageHandler.go index da05af24cfca4403926f627bf0c54d951eb216a0..ad729ee8cf9c43116903f5cb4727e48724b78a56 100644 --- a/indexedDbWorker/messageHandler.go +++ b/indexedDb/messageHandler.go @@ -7,13 +7,13 @@ //go:build js && wasm -package indexedDbWorker +package indexedDb2 import ( "encoding/json" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" "gitlab.com/elixxir/xxdk-wasm/utils" "sync" "syscall/js" diff --git a/indexedDbWorker/utils.go b/indexedDb/utils.go similarity index 99% rename from indexedDbWorker/utils.go rename to indexedDb/utils.go index 321dd082214e7ab88ed6351442882317426f4e87..87696d0f8be15f8584d7c1dbf45a215767e63467 100644 --- a/indexedDbWorker/utils.go +++ b/indexedDb/utils.go @@ -10,7 +10,7 @@ // This file contains several generic IndexedDB helper functions that // may be useful for any IndexedDB implementations. -package indexedDbWorker +package indexedDb2 import ( "context" @@ -31,7 +31,7 @@ const ( ErrDoesNotExist = "result is undefined" ) -// NewContext builds a context for indexedDb operations. +// NewContext builds a context for indexedDbWorker operations. func NewContext() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), dbTimeout) } diff --git a/indexedDbWorker/utils_test.go b/indexedDb/utils_test.go similarity index 98% rename from indexedDbWorker/utils_test.go rename to indexedDb/utils_test.go index 62851adb196907b392a85d82b5e333990cc26122..279a33f9f86cfcaed014a9fe0c64db34d4a5cbac 100644 --- a/indexedDbWorker/utils_test.go +++ b/indexedDb/utils_test.go @@ -7,7 +7,7 @@ //go:build js && wasm -package indexedDbWorker +package indexedDb2 import ( "github.com/hack-pad/go-indexeddb/idb" diff --git a/indexedDbWorker/channels/implementation.go b/indexedDbWorker/channels/implementation.go index 96e4351964a024155041a92892d2b6328c932c4e..fd7e13000c496a25d8bb1b226efa9490e526ee55 100644 --- a/indexedDbWorker/channels/implementation.go +++ b/indexedDbWorker/channels/implementation.go @@ -7,28 +7,22 @@ //go:build js && wasm -package main +package channels import ( "crypto/ed25519" - "encoding/base64" "encoding/json" - "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" - "strings" - "sync" - "syscall/js" "time" - "github.com/hack-pad/go-indexeddb/idb" + "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" + "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" - cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/message" - "gitlab.com/elixxir/xxdk-wasm/utils" "gitlab.com/xx_network/primitives/id" ) @@ -36,108 +30,23 @@ import ( // system passed an object that adheres to in order to get events on the // channel. type wasmModel struct { - db *idb.Database - cipher cryptoChannel.Cipher - receivedMessageCB MessageReceivedCallback - updateMux sync.Mutex + wh *indexedDb.WorkerHandler } // JoinChannel is called whenever a channel is joined locally. func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { - parentErr := errors.New("failed to JoinChannel") - - // Build object - newChannel := Channel{ - ID: channel.ReceptionID.Marshal(), - Name: channel.Name, - Description: channel.Description, - } - - // Convert to jsObject - newChannelJson, err := json.Marshal(&newChannel) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to marshal Channel: %+v", err)) - return - } - channelObj, err := utils.JsonToJS(newChannelJson) + data, err := json.Marshal(channel) if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to marshal Channel: %+v", err)) + jww.ERROR.Printf("Could not JSON marshal broadcast.Channel: %+v", err) return } - _, err = indexedDbWorker.Put(w.db, channelsStoreName, channelObj) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to put Channel: %+v", err)) - } + w.wh.SendMessage(indexedDb.JoinChannelTag, data, nil) } // LeaveChannel is called whenever a channel is left locally. func (w *wasmModel) LeaveChannel(channelID *id.ID) { - parentErr := errors.New("failed to LeaveChannel") - - // Delete the channel from storage - err := indexedDbWorker.Delete(w.db, channelsStoreName, - js.ValueOf(channelID.String())) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to delete Channel: %+v", err)) - return - } - - // Clean up lingering data - err = w.deleteMsgByChannel(channelID) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Deleting Channel's Message data failed: %+v", err)) - return - } - jww.DEBUG.Printf("Successfully deleted channel: %s", channelID) -} - -// deleteMsgByChannel is a private helper that uses messageStoreChannelIndex -// to delete all Message with the given Channel ID. -func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error { - parentErr := errors.New("failed to deleteMsgByChannel") - - // Prepare the Transaction - txn, err := w.db.Transaction(idb.TransactionReadWrite, messageStoreName) - if err != nil { - return errors.WithMessagef(parentErr, - "Unable to create Transaction: %+v", err) - } - store, err := txn.ObjectStore(messageStoreName) - if err != nil { - return errors.WithMessagef(parentErr, - "Unable to get ObjectStore: %+v", err) - } - index, err := store.Index(messageStoreChannelIndex) - if err != nil { - return errors.WithMessagef(parentErr, - "Unable to get Index: %+v", err) - } - - // Perform the operation - channelIdStr := base64.StdEncoding.EncodeToString(channelID.Marshal()) - keyRange, err := idb.NewKeyRangeOnly(js.ValueOf(channelIdStr)) - cursorRequest, err := index.OpenCursorRange(keyRange, idb.CursorNext) - if err != nil { - return errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err) - } - ctx, cancel := indexedDbWorker.NewContext() - err = cursorRequest.Iter(ctx, - func(cursor *idb.CursorWithValue) error { - _, err := cursor.Delete() - return err - }) - cancel() - if err != nil { - return errors.WithMessagef(parentErr, - "Unable to delete Message data: %+v", err) - } - return nil + w.wh.SendMessage(indexedDb.LeaveChannelTag, channelID.Marshal(), nil) } // ReceiveMessage is called whenever a message is received on a given channel. @@ -148,30 +57,58 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID, nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 { - textBytes := []byte(text) - var err error + msg := channels.ModelMessage{ + Nickname: nickname, + MessageID: messageID, + ChannelID: channelID, + Timestamp: timestamp, + Lease: lease, + Status: status, + Hidden: hidden, + Pinned: false, + Content: []byte(text), + Type: mType, + Round: round.ID, + PubKey: pubKey, + CodesetVersion: codeset, + DmToken: dmToken, + } - // Handle encryption, if it is present - if w.cipher != nil { - textBytes, err = w.cipher.Encrypt([]byte(text)) - if err != nil { - jww.ERROR.Printf("Failed to encrypt Message: %+v", err) - return 0 - } + data, err := json.Marshal(msg) + if err != nil { + jww.ERROR.Printf( + "Could not JSON marshal payload for ReceiveMessage: %+v", err) + return 0 } - msgToInsert := buildMessage( - channelID.Marshal(), messageID.Bytes(), nil, nickname, - textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, - false, hidden, status) + uuidChan := make(chan uint64) + w.wh.SendMessage(indexedDb.ReceiveMessageTag, data, func(data []byte) { + var uuid uint64 + err = json.Unmarshal(data, &uuid) + if err != nil { + jww.ERROR.Printf( + "Could not JSON unmarshal response to ReceiveMessage: %+v", err) + uuidChan <- 0 + } + uuidChan <- uuid + }) - uuid, err := w.receiveHelper(msgToInsert, false) - if err != nil { - jww.ERROR.Printf("Failed to receive Message: %+v", err) + select { + case uuid := <-uuidChan: + return uuid + case <-time.After(indexedDb.ResponseTimeout): + jww.ERROR.Printf("Timed out after %s waiting for response from the "+ + "worker about ReceiveMessage", indexedDb.ResponseTimeout) } - go w.receivedMessageCB(uuid, channelID, false) - return uuid + return 0 +} + +// ReceiveReplyMessage is JSON marshalled and sent to the worker for +// [wasmModel.ReceiveReply]. +type ReceiveReplyMessage struct { + ReactionTo message.ID `json:"replyTo"` + channels.ModelMessage `json:"message"` } // ReceiveReply is called whenever a message is received that is a reply on a @@ -185,29 +122,54 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 { - textBytes := []byte(text) - var err error + msg := ReceiveReplyMessage{ + ReactionTo: replyTo, + ModelMessage: channels.ModelMessage{ + Nickname: nickname, + MessageID: messageID, + ChannelID: channelID, + Timestamp: timestamp, + Lease: lease, + Status: status, + Hidden: hidden, + Pinned: false, + Content: []byte(text), + Type: mType, + Round: round.ID, + PubKey: pubKey, + CodesetVersion: codeset, + DmToken: dmToken, + }, + } + + data, err := json.Marshal(msg) + if err != nil { + jww.ERROR.Printf( + "Could not JSON marshal payload for ReceiveReply: %+v", err) + return 0 + } - // Handle encryption, if it is present - if w.cipher != nil { - textBytes, err = w.cipher.Encrypt([]byte(text)) + uuidChan := make(chan uint64) + w.wh.SendMessage(indexedDb.ReceiveReplyTag, data, func(data []byte) { + var uuid uint64 + err = json.Unmarshal(data, &uuid) if err != nil { - jww.ERROR.Printf("Failed to encrypt Message: %+v", err) - return 0 + jww.ERROR.Printf( + "Could not JSON unmarshal response to ReceiveReply: %+v", err) + uuidChan <- 0 } - } + uuidChan <- uuid + }) - msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(), - replyTo.Bytes(), nickname, textBytes, pubKey, dmToken, codeset, - timestamp, lease, round.ID, mType, hidden, false, status) - - uuid, err := w.receiveHelper(msgToInsert, false) - - if err != nil { - jww.ERROR.Printf("Failed to receive reply: %+v", err) + select { + case uuid := <-uuidChan: + return uuid + case <-time.After(indexedDb.ResponseTimeout): + jww.ERROR.Printf("Timed out after %s waiting for response from the "+ + "worker about ReceiveReply", indexedDb.ResponseTimeout) } - go w.receivedMessageCB(uuid, channelID, false) - return uuid + + return 0 } // ReceiveReaction is called whenever a reaction to a message is received on a @@ -221,29 +183,79 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 { - textBytes := []byte(reaction) - var err error - // Handle encryption, if it is present - if w.cipher != nil { - textBytes, err = w.cipher.Encrypt([]byte(reaction)) + msg := ReceiveReplyMessage{ + ReactionTo: reactionTo, + ModelMessage: channels.ModelMessage{ + Nickname: nickname, + MessageID: messageID, + ChannelID: channelID, + Timestamp: timestamp, + Lease: lease, + Status: status, + Hidden: hidden, + Pinned: false, + Content: []byte(reaction), + Type: mType, + Round: round.ID, + PubKey: pubKey, + CodesetVersion: codeset, + DmToken: dmToken, + }, + } + + data, err := json.Marshal(msg) + if err != nil { + jww.ERROR.Printf( + "Could not JSON marshal payload for ReceiveReaction: %+v", err) + return 0 + } + + uuidChan := make(chan uint64) + w.wh.SendMessage(indexedDb.ReceiveReactionTag, data, func(data []byte) { + var uuid uint64 + err = json.Unmarshal(data, &uuid) if err != nil { - jww.ERROR.Printf("Failed to encrypt Message: %+v", err) - return 0 + 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(indexedDb.ResponseTimeout): + jww.ERROR.Printf("Timed out after %s waiting for response from the "+ + "worker about ReceiveReply", indexedDb.ResponseTimeout) } - msgToInsert := buildMessage( - channelID.Marshal(), messageID.Bytes(), reactionTo.Bytes(), nickname, - textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, - false, hidden, status) + return 0 +} - uuid, err := w.receiveHelper(msgToInsert, false) - if err != nil { - jww.ERROR.Printf("Failed to receive reaction: %+v", err) - } - go w.receivedMessageCB(uuid, channelID, false) - return uuid +// MessageUpdateInfo is JSON marshalled and sent to the worker for +// [wasmModel.UpdateFromMessageID] and [wasmModel.UpdateFromUUID]. +type MessageUpdateInfo struct { + UUID uint64 `json:"uuid"` + + MessageID message.ID `json:"messageID"` + MessageIDSet bool `json:"messageIDSet"` + + Timestamp time.Time `json:"timestamp"` + TimestampSet bool `json:"timestampSet"` + + RoundID id.Round `json:"round"` + RoundIDSet bool `json:"roundIDSet"` + + Pinned bool `json:"pinned"` + PinnedSet bool `json:"pinnedSet"` + + Hidden bool `json:"hidden"` + HiddenSet bool `json:"hiddenSet"` + + Status channels.SentStatus `json:"status"` + StatusSet bool `json:"statusSet"` } // UpdateFromUUID is called whenever a message at the UUID is modified. @@ -254,31 +266,40 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID, func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, status *channels.SentStatus) { - parentErr := errors.New("failed to UpdateFromUUID") - - // FIXME: this is a bit of race condition without the mux. - // This should be done via the transactions (i.e., make a - // special version of receiveHelper) - w.updateMux.Lock() - defer w.updateMux.Unlock() - - // Convert messageID to the key generated by json.Marshal - key := js.ValueOf(uuid) + msg := MessageUpdateInfo{UUID: uuid} + if messageID != nil { + msg.MessageID = *messageID + msg.MessageIDSet = true + } + if timestamp != nil { + msg.Timestamp = *timestamp + msg.TimestampSet = true + } + if round != nil { + msg.RoundID = round.ID + msg.RoundIDSet = true + } + if pinned != nil { + msg.Pinned = *pinned + msg.PinnedSet = true + } + if hidden != nil { + msg.Hidden = *hidden + msg.HiddenSet = true + } + if status != nil { + msg.Status = *status + msg.StatusSet = true + } - // Use the key to get the existing Message - currentMsg, err := indexedDbWorker.Get(w.db, messageStoreName, key) + data, err := json.Marshal(msg) if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to get message: %+v", err)) + jww.ERROR.Printf( + "Could not JSON marshal payload for UpdateFromUUID: %+v", err) return } - _, err = w.updateMessage(utils.JsToJson(currentMsg), messageID, timestamp, - round, pinned, hidden, status) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to updateMessage: %+v", err)) - } + w.wh.SendMessage(indexedDb.UpdateFromUUIDTag, data, nil) } // UpdateFromMessageID is called whenever a message with the message ID is @@ -293,222 +314,109 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, func (w *wasmModel) UpdateFromMessageID(messageID message.ID, timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, status *channels.SentStatus) uint64 { - parentErr := errors.New("failed to UpdateFromMessageID") - - // FIXME: this is a bit of race condition without the mux. - // This should be done via the transactions (i.e., make a - // special version of receiveHelper) - w.updateMux.Lock() - defer w.updateMux.Unlock() - - msgIDStr := base64.StdEncoding.EncodeToString(messageID.Marshal()) - currentMsgObj, err := indexedDbWorker.GetIndex(w.db, messageStoreName, - messageStoreMessageIndex, js.ValueOf(msgIDStr)) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Failed to get message by index: %+v", err)) - return 0 - } - currentMsg := utils.JsToJson(currentMsgObj) - uuid, err := w.updateMessage(currentMsg, &messageID, timestamp, - round, pinned, hidden, status) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to updateMessage: %+v", err)) - } - return uuid -} - -// updateMessage is a helper for updating a stored message. -func (w *wasmModel) updateMessage(currentMsgJson string, messageID *message.ID, - timestamp *time.Time, round *rounds.Round, pinned, hidden *bool, - status *channels.SentStatus) (uint64, error) { - - newMessage := &Message{} - err := json.Unmarshal([]byte(currentMsgJson), newMessage) - if err != nil { - return 0, err - } - - if status != nil { - newMessage.Status = uint8(*status) - } - if messageID != nil { - newMessage.MessageID = messageID.Bytes() + msg := MessageUpdateInfo{MessageID: messageID, MessageIDSet: true} + if timestamp != nil { + msg.Timestamp = *timestamp + msg.TimestampSet = true } - if round != nil { - newMessage.Round = uint64(round.ID) - } - - if timestamp != nil { - newMessage.Timestamp = *timestamp + msg.RoundID = round.ID + msg.RoundIDSet = true } - if pinned != nil { - newMessage.Pinned = *pinned + msg.Pinned = *pinned + msg.PinnedSet = true } - if hidden != nil { - newMessage.Hidden = *hidden - } - - // Store the updated Message - uuid, err := w.receiveHelper(newMessage, true) - if err != nil { - return 0, err + msg.Hidden = *hidden + msg.HiddenSet = true } - channelID := &id.ID{} - copy(channelID[:], newMessage.ChannelID) - go w.receivedMessageCB(uuid, channelID, true) - - return uuid, nil -} - -// buildMessage is a private helper that converts typical [channels.EventModel] -// inputs into a basic Message structure for insertion into storage. -// -// NOTE: ID is not set inside this function because we want to use the -// autoincrement key by default. If you are trying to overwrite an existing -// message, then you need to set it manually yourself. -func buildMessage(channelID, messageID, parentID []byte, nickname string, - text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, - timestamp time.Time, lease time.Duration, round id.Round, - mType channels.MessageType, pinned, hidden bool, - status channels.SentStatus) *Message { - return &Message{ - MessageID: messageID, - Nickname: nickname, - ChannelID: channelID, - ParentMessageID: parentID, - Timestamp: timestamp, - Lease: lease, - Status: uint8(status), - Hidden: hidden, - Pinned: pinned, - Text: text, - Type: uint16(mType), - Round: uint64(round), - // User Identity Info - Pubkey: pubKey, - DmToken: dmToken, - CodesetVersion: codeset, + if status != nil { + msg.Status = *status + msg.StatusSet = true } -} -// receiveHelper is a private helper for receiving any sort of message. -func (w *wasmModel) receiveHelper( - newMessage *Message, isUpdate bool) (uint64, error) { - // Convert to jsObject - newMessageJson, err := json.Marshal(newMessage) + data, err := json.Marshal(msg) if err != nil { - return 0, errors.Errorf("Unable to marshal Message: %+v", err) - } - messageObj, err := utils.JsonToJS(newMessageJson) - if err != nil { - return 0, errors.Errorf("Unable to marshal Message: %+v", err) + jww.ERROR.Printf( + "Could not JSON marshal payload for UpdateFromMessageID: %+v", err) + return 0 } - // Unset the primaryKey for inserts so that it can be auto-populated and - // incremented - if !isUpdate { - messageObj.Delete("id") - } + uuidChan := make(chan uint64) + w.wh.SendMessage(indexedDb.UpdateFromMessageIDTag, data, func(data []byte) { + var uuid uint64 + err = json.Unmarshal(data, &uuid) + if err != nil { + jww.ERROR.Printf( + "Could not JSON unmarshal response to UpdateFromMessageID: %+v", err) + uuidChan <- 0 + } + uuidChan <- uuid + }) - // Store message to database - result, err := indexedDbWorker.Put(w.db, messageStoreName, messageObj) - if err != nil && !strings.Contains(err.Error(), - "at least one key does not satisfy the uniqueness requirements") { - // Only return non-unique constraint errors so that the case - // below this one can be hit and handle duplicate entries properly. - return 0, errors.Errorf("Unable to put Message: %+v", err) + select { + case uuid := <-uuidChan: + return uuid + case <-time.After(indexedDb.ResponseTimeout): + jww.ERROR.Printf("Timed out after %s waiting for response from the "+ + "worker about ReceiveReply", indexedDb.ResponseTimeout) } - // NOTE: Sometimes the insert fails to return an error but hits a duplicate - // insert, so this fallthrough returns the UUID entry in that case. - if result.IsUndefined() { - msgID := message.ID{} - copy(msgID[:], newMessage.MessageID) - msg, errLookup := w.msgIDLookup(msgID) - if errLookup == nil && msg.ID != 0 { - return msg.ID, nil - } - return 0, errors.Errorf("uuid lookup failure: %+v", err) - } - uuid := uint64(result.Int()) - jww.DEBUG.Printf("Successfully stored message %d", uuid) + return 0 +} - return uuid, nil +// GetMessageMessage is JSON marshalled and sent to the worker for +// [wasmModel.GetMessage]. +type GetMessageMessage struct { + Message channels.ModelMessage `json:"message"` + Error string `json:"error"` } // GetMessage returns the message with the given [channel.MessageID]. func (w *wasmModel) GetMessage( messageID message.ID) (channels.ModelMessage, error) { - lookupResult, err := w.msgIDLookup(messageID) - if err != nil { - return channels.ModelMessage{}, err - } - - var channelId *id.ID - if lookupResult.ChannelID != nil { - channelId, err = id.Unmarshal(lookupResult.ChannelID) + msgChan := make(chan GetMessageMessage) + w.wh.SendMessage(indexedDb.GetMessageTag, messageID.Marshal(), func(data []byte) { + var msg GetMessageMessage + err := json.Unmarshal(data, &msg) if err != nil { - return channels.ModelMessage{}, err + jww.ERROR.Printf( + "Could not JSON unmarshal response to GetMessage: %+v", err) } - } + msgChan <- msg + }) - var parentMsgId message.ID - if lookupResult.ParentMessageID != nil { - parentMsgId, err = message.UnmarshalID(lookupResult.ParentMessageID) - if err != nil { - return channels.ModelMessage{}, err + select { + case msg := <-msgChan: + if msg.Error != "" { + return channels.ModelMessage{}, errors.New(msg.Error) } + return msg.Message, nil + case <-time.After(indexedDb.ResponseTimeout): + return channels.ModelMessage{}, errors.Errorf("timed out after %s "+ + "waiting for response from the worker about GetMessage", + indexedDb.ResponseTimeout) } - - return channels.ModelMessage{ - UUID: lookupResult.ID, - Nickname: lookupResult.Nickname, - MessageID: messageID, - ChannelID: channelId, - ParentMessageID: parentMsgId, - Timestamp: lookupResult.Timestamp, - Lease: lookupResult.Lease, - Status: channels.SentStatus(lookupResult.Status), - Hidden: lookupResult.Hidden, - Pinned: lookupResult.Pinned, - Content: lookupResult.Text, - Type: channels.MessageType(lookupResult.Type), - Round: id.Round(lookupResult.Round), - PubKey: lookupResult.Pubkey, - CodesetVersion: lookupResult.CodesetVersion, - }, nil } // DeleteMessage removes a message with the given messageID from storage. func (w *wasmModel) DeleteMessage(messageID message.ID) error { - msgId := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes())) - return indexedDbWorker.DeleteIndex(w.db, messageStoreName, - messageStoreMessageIndex, pkeyName, msgId) -} - -// msgIDLookup gets the UUID of the Message with the given messageID. -func (w *wasmModel) msgIDLookup(messageID message.ID) (*Message, error) { - msgIDStr := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes())) - resultObj, err := indexedDbWorker.GetIndex(w.db, messageStoreName, - messageStoreMessageIndex, msgIDStr) - if err != nil { - return nil, err - } else if resultObj.IsUndefined() { - return nil, errors.Errorf("no message for %s found", msgIDStr) - } + errChan := make(chan error) + w.wh.SendMessage(indexedDb.DeleteMessageTag, messageID.Marshal(), func(data []byte) { + if data != nil { + errChan <- errors.New(string(data)) + } else { + errChan <- nil + } + }) - // Process result into string - resultMsg := &Message{} - err = json.Unmarshal([]byte(utils.JsToJson(resultObj)), resultMsg) - if err != nil { - return nil, err + select { + case err := <-errChan: + return err + case <-time.After(indexedDb.ResponseTimeout): + return errors.Errorf("timed out after %s waiting for response from "+ + "the worker about DeleteMessage", indexedDb.ResponseTimeout) } - return resultMsg, nil - } diff --git a/indexedDbWorker/channels/init.go b/indexedDbWorker/channels/init.go index a1eba82bae80dd8ad035669bd86def850b8dfb88..a1bc72917e6dac1fb1ec6c7543c5c797f2e625eb 100644 --- a/indexedDbWorker/channels/init.go +++ b/indexedDbWorker/channels/init.go @@ -7,248 +7,168 @@ //go:build js && wasm -package main +package channels import ( - "github.com/hack-pad/go-indexeddb/idb" + "encoding/json" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/channels" cryptoChannel "gitlab.com/elixxir/crypto/channel" - "gitlab.com/elixxir/xxdk-wasm/indexedDb" "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" + "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/xx_network/primitives/id" - "syscall/js" "time" ) -const ( - // databaseSuffix is the suffix to be appended to the name of - // the database. - databaseSuffix = "_speakeasy" - - // currentVersion is the current version of the IndexDb - // runtime. Used for migration purposes. - currentVersion uint = 1 -) +// WorkerJavascriptFileURL is the URL of the script the worker will execute to +// launch the worker WASM binary. It must obey the same-origin policy. +const WorkerJavascriptFileURL = "/integrations/assets/dmIndexedDbWorker.js" // MessageReceivedCallback is called any time a message is received or updated. // // update is true if the row is old and was edited. type MessageReceivedCallback func(uuid uint64, channelID *id.ID, update bool) +// NewWASMEventModelBuilder returns an EventModelBuilder which allows +// the channel manager to define the path but the callback is the same +// across the board. +func NewWASMEventModelBuilder(encryption cryptoChannel.Cipher, + cb MessageReceivedCallback) channels.EventModelBuilder { + fn := func(path string) (channels.EventModel, error) { + return NewWASMEventModel(path, encryption, cb) + } + return fn +} + +// NewWASMEventModelMessage is JSON marshalled and sent to the worker for +// [NewWASMEventModel]. +type NewWASMEventModelMessage struct { + Path string `json:"path"` + EncryptionJSON string `json:"encryptionJSON"` +} + // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel. // The name should be a base64 encoding of the users public key. func NewWASMEventModel(path string, encryption cryptoChannel.Cipher, - cb MessageReceivedCallback, storeEncryptionStatus storeEncryptionStatusFn) ( - channels.EventModel, error) { - databaseName := path + databaseSuffix - return newWASMModel(databaseName, encryption, cb, storeEncryptionStatus) -} + cb MessageReceivedCallback) (channels.EventModel, error) { -// storeEncryptionStatusFn matches storage.StoreIndexedDbEncryptionStatus so -// that the data can be sent between the worker and main thread. -type storeEncryptionStatusFn func( - databaseName string, encryptionStatus bool) (bool, error) - -// newWASMModel creates the given [idb.Database] and returns a wasmModel. -func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, - cb MessageReceivedCallback, storeEncryptionStatus storeEncryptionStatusFn) ( - *wasmModel, error) { - // Attempt to open database object - ctx, cancel := indexedDbWorker.NewContext() - defer cancel() - openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion, - func(db *idb.Database, oldVersion, newVersion uint) error { - if oldVersion == newVersion { - jww.INFO.Printf("IndexDb version is current: v%d", - newVersion) - return nil - } - - jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d", - oldVersion, newVersion) - - if oldVersion == 0 && newVersion >= 1 { - err := v1Upgrade(db) - if err != nil { - return err - } - oldVersion = 1 - } - - // if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 } - return nil - }) + // TODO: bring in URL and name from caller + wh, err := indexedDb.NewWorkerHandler( + WorkerJavascriptFileURL, "channelsIndexedDb") if err != nil { return nil, err } - // Wait for database open to finish - db, err := openRequest.Await(ctx) - if err != nil { - return nil, err - } + // Register handler to manage messages for the MessageReceivedCallback + wh.RegisterHandler(indexedDb.GetMessageTag, indexedDb.InitID, false, + messageReceivedCallbackHandler(cb)) + + // Register handler to manage checking encryption status from local storage + wh.RegisterHandler(indexedDb.EncryptionStatusTag, indexedDb.InitID, false, + checkDbEncryptionStatusHandler(wh)) - // Save the encryption status to storage - encryptionStatus := encryption != nil - loadedEncryptionStatus, err := - storeEncryptionStatus(databaseName, encryptionStatus) + encryptionJSON, err := json.Marshal(encryption) if err != nil { return nil, err } - // Verify encryption status does not change - if encryptionStatus != loadedEncryptionStatus { - return nil, errors.New( - "Cannot load database with different encryption status.") - } else if !encryptionStatus { - jww.WARN.Printf("IndexedDb encryption disabled!") + message := NewWASMEventModelMessage{ + Path: path, + EncryptionJSON: string(encryptionJSON), } - // Attempt to ensure the database has been properly initialized - openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion, - func(db *idb.Database, oldVersion, newVersion uint) error { - return nil - }) + payload, err := json.Marshal(message) if err != nil { return nil, err } - // Wait for database open to finish - db, err = openRequest.Await(ctx) - if err != nil { - return nil, err - } - wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption} - return wrapper, nil -} + errChan := make(chan string) + wh.SendMessage(indexedDb.NewWASMEventModelTag, payload, func(data []byte) { + errChan <- string(data) + }) -// v1Upgrade performs the v0 -> v1 database upgrade. -// -// This can never be changed without permanently breaking backwards -// compatibility. -func v1Upgrade(db *idb.Database) error { - storeOpts := idb.ObjectStoreOptions{ - KeyPath: js.ValueOf(pkeyName), - AutoIncrement: true, - } - indexOpts := idb.IndexOptions{ - Unique: false, - MultiEntry: false, + select { + case workerErr := <-errChan: + if workerErr != "" { + return nil, errors.New(workerErr) + } + case <-time.After(indexedDb.ResponseTimeout): + return nil, errors.Errorf("timed out after %s waiting for indexedDB "+ + "database in worker to intialize", indexedDb.ResponseTimeout) } - // Build Message ObjectStore and Indexes - messageStore, err := db.CreateObjectStore(messageStoreName, storeOpts) - if err != nil { - return err - } - _, err = messageStore.CreateIndex(messageStoreMessageIndex, - js.ValueOf(messageStoreMessage), - idb.IndexOptions{ - Unique: true, - MultiEntry: false, - }) - if err != nil { - return err - } - _, err = messageStore.CreateIndex(messageStoreChannelIndex, - js.ValueOf(messageStoreChannel), indexOpts) - if err != nil { - return err - } - _, err = messageStore.CreateIndex(messageStoreParentIndex, - js.ValueOf(messageStoreParent), indexOpts) - if err != nil { - return err - } - _, err = messageStore.CreateIndex(messageStoreTimestampIndex, - js.ValueOf(messageStoreTimestamp), indexOpts) - if err != nil { - return err - } - _, err = messageStore.CreateIndex(messageStorePinnedIndex, - js.ValueOf(messageStorePinned), indexOpts) - if err != nil { - return err - } + return &wasmModel{wh}, nil +} - // Build Channel ObjectStore - _, err = db.CreateObjectStore(channelsStoreName, storeOpts) - if err != nil { - return err - } +// MessageReceivedCallbackMessage is JSON marshalled and received from the +// worker for the [MessageReceivedCallback] callback. +type MessageReceivedCallbackMessage struct { + UUID uint64 `json:"uuid"` + ChannelID *id.ID `json:"channelID"` + Update bool `json:"update"` +} - // Get the database name and save it to storage - if databaseName, err2 := db.Name(); err2 != nil { - return err2 - } else if err = storeDatabaseName(databaseName); err != nil { - return err +// messageReceivedCallbackHandler returns a handler to manage messages for the +// MessageReceivedCallback. +func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte) { + return func(data []byte) { + var msg MessageReceivedCallbackMessage + err := json.Unmarshal(data, &msg) + if err != nil { + jww.ERROR.Printf("Failed to JSON unmarshal "+ + "MessageReceivedCallback message from worker: %+v", err) + return + } + cb(msg.UUID, msg.ChannelID, msg.Update) } +} - return nil +// EncryptionStatusMessage is JSON marshalled and received from the worker when +// the database checks if it is encrypted. +type EncryptionStatusMessage struct { + DatabaseName string `json:"databaseName"` + EncryptionStatus bool `json:"encryptionStatus"` } -// hackTestDb is a horrible function that exists as the result of an extremely -// long discussion about why initializing the IndexedDb sometimes silently -// fails. It ultimately tries to prevent an unrecoverable situation by actually -// inserting some nonsense data and then checking to see if it persists. -// If this function still exists in 2023, god help us all. Amen. -func (w *wasmModel) hackTestDb() error { - testMessage := &Message{ - ID: 0, - Nickname: "test", - MessageID: id.DummyUser.Marshal(), - } - msgId, helper := w.receiveHelper(testMessage, false) - if helper != nil { - return helper - } - result, err := indexedDbWorker.Get(w.db, messageStoreName, js.ValueOf(msgId)) - if err != nil { - return err - } - if result.IsUndefined() { - return errors.Errorf("Failed to test db, record not present") - } - return nil +// EncryptionStatusReply is JSON marshalled and sent to the worker is response +// to the [EncryptionStatusMessage]. +type EncryptionStatusReply struct { + EncryptionStatus bool `json:"encryptionStatus"` + Error string `json:"error"` } -// storeDatabaseName sends the database name to storage.StoreIndexedDb in the -// main thread to be stored in localstorage and waits for the error to be -// returned. -// -// The function specified below is a placeholder until set by -// registerDatabaseNameStore. registerDatabaseNameStore must be called before -// storeDatabaseName. -var storeDatabaseName = func(databaseName string) error { return nil } - -// RegisterDatabaseNameStore sets storeDatabaseName to send the database to -// storage.StoreIndexedDb in the main thread when called and registers a handler -// to listen for the response. -func RegisterDatabaseNameStore(m *manager) { - storeDatabaseNameResponseChan := make(chan []byte) - // Register handler - m.mh.RegisterHandler(indexedDb.StoreDatabaseNameTag, func(data []byte) []byte { - storeDatabaseNameResponseChan <- data - return nil - }) +// checkDbEncryptionStatusHandler returns a handler to manage checking +// encryption status from local storage. +func checkDbEncryptionStatusHandler(wh *indexedDb.WorkerHandler) func(data []byte) { + return func(data []byte) { + // Unmarshal received message + var msg EncryptionStatusMessage + err := json.Unmarshal(data, &msg) + if err != nil { + jww.ERROR.Printf("Failed to JSON unmarshal "+ + "EncryptionStatusMessage message from worker: %+v", err) + return + } - storeDatabaseName = func(databaseName string) error { - m.mh.SendResponse(indexedDb.StoreDatabaseNameTag, indexedDb.InitID, - []byte(databaseName)) - - // Wait for response - select { - case response := <-storeDatabaseNameResponseChan: - if len(response) > 0 { - return errors.New(string(response)) - } - case <-time.After(indexedDb.ResponseTimeout): - return errors.Errorf("timed out after %s waiting for "+ - "response about storing the database name in local "+ - "storage in the main thread", indexedDb.ResponseTimeout) + // Pass message values to storage + loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( + msg.DatabaseName, msg.EncryptionStatus) + var reply EncryptionStatusReply + if err != nil { + reply.Error = err.Error() + } else { + reply.EncryptionStatus = loadedEncryptionStatus } - return nil + + // Return response + statusData, err := json.Marshal(reply) + if err != nil { + jww.ERROR.Printf( + "Failed to JSON marshal EncryptionStatusReply: %+v", err) + return + } + + wh.SendMessage(indexedDb.EncryptionStatusTag, statusData, nil) } } diff --git a/indexedDbWorker/dm/implementation.go b/indexedDbWorker/dm/implementation.go index 9c8f5a5be24b76fa344c3100371b1c660327dade..0bbdcfd77ad0d53294c7d7ed27c5dde4c14cdd63 100644 --- a/indexedDbWorker/dm/implementation.go +++ b/indexedDbWorker/dm/implementation.go @@ -7,375 +7,240 @@ //go:build js && wasm -package main +package channelEventModel import ( "crypto/ed25519" "encoding/json" - "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" - "strings" - "sync" - "syscall/js" "time" - "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/cmix/rounds" "gitlab.com/elixxir/client/v4/dm" - "gitlab.com/elixxir/xxdk-wasm/utils" - "gitlab.com/xx_network/primitives/id" - - "github.com/hack-pad/go-indexeddb/idb" - cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/message" + "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" ) // wasmModel implements dm.EventModel interface, which uses the channels system // passed an object that adheres to in order to get events on the channel. type wasmModel struct { - db *idb.Database - cipher cryptoChannel.Cipher - receivedMessageCB MessageReceivedCallback - updateMux sync.Mutex -} - -// joinConversation is used for joining new conversations. -func (w *wasmModel) joinConversation(nickname string, - pubKey ed25519.PublicKey, dmToken uint32, codeset uint8) error { - parentErr := errors.New("failed to joinConversation") - - // Build object - newConvo := Conversation{ - Pubkey: pubKey, - Nickname: nickname, - Token: dmToken, - CodesetVersion: codeset, - Blocked: false, - } - - // Convert to jsObject - newConvoJson, err := json.Marshal(&newConvo) - if err != nil { - return errors.WithMessagef(parentErr, - "Unable to marshal Conversation: %+v", err) - } - convoObj, err := utils.JsonToJS(newConvoJson) - if err != nil { - return errors.WithMessagef(parentErr, - "Unable to marshal Conversation: %+v", err) - } - - _, err = indexedDbWorker.Put(w.db, conversationStoreName, convoObj) - if err != nil { - return errors.WithMessagef(parentErr, - "Unable to put Conversation: %+v", err) - } - return nil + wh *indexedDb.WorkerHandler } -// buildMessage is a private helper that converts typical dm.EventModel inputs -// into a basic Message structure for insertion into storage. -// -// NOTE: ID is not set inside this function because we want to use the -// autoincrement key by default. If you are trying to overwrite an existing -// message, then you need to set it manually yourself. -func buildMessage(messageID, parentID, text []byte, pubKey ed25519.PublicKey, - timestamp time.Time, round id.Round, mType dm.MessageType, - status dm.Status) *Message { - return &Message{ - MessageID: messageID, - ConversationPubKey: pubKey, - ParentMessageID: parentID, - Timestamp: timestamp, - Status: uint8(status), - Text: text, - Type: uint16(mType), - Round: uint64(round), - } +// TransferMessage is JSON marshalled and sent to the worker. +type TransferMessage struct { + UUID uint64 `json:"uuid"` + MessageID message.ID `json:"messageID"` + ReactionTo message.ID `json:"reactionTo"` + Nickname string `json:"nickname"` + Text []byte `json:"text"` + PubKey ed25519.PublicKey `json:"pubKey"` + DmToken uint32 `json:"dmToken"` + Codeset uint8 `json:"codeset"` + Timestamp time.Time `json:"timestamp"` + Round rounds.Round `json:"round"` + MType dm.MessageType `json:"mType"` + Status dm.Status `json:"status"` } func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, round rounds.Round, mType dm.MessageType, status dm.Status) uint64 { - parentErr := errors.New("failed to Receive") - - // If there is no extant Conversation, create one. - _, err := indexedDbWorker.Get( - w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) - if err != nil { - if strings.Contains(err.Error(), indexedDbWorker.ErrDoesNotExist) { - err = w.joinConversation(nickname, pubKey, dmToken, codeset) - if err != nil { - jww.ERROR.Printf("%+v", err) - } - } else { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to get Conversation: %+v", err)) - } + msg := TransferMessage{ + MessageID: messageID, + Nickname: nickname, + Text: text, + PubKey: pubKey, + DmToken: dmToken, + Codeset: codeset, + Timestamp: timestamp, + Round: round, + MType: mType, + Status: status, + } + + data, err := json.Marshal(msg) + if err != nil { + jww.ERROR.Printf( + "Could not JSON marshal payload for TransferMessage: %+v", err) return 0 - } else { - jww.DEBUG.Printf("Conversation with %s already joined", nickname) } - // Handle encryption, if it is present - if w.cipher != nil { - text, err = w.cipher.Encrypt(text) + uuidChan := make(chan uint64) + w.wh.SendMessage(indexedDb.ReceiveTag, data, func(data []byte) { + var uuid uint64 + err = json.Unmarshal(data, &uuid) if err != nil { - jww.ERROR.Printf("Failed to encrypt Message: %+v", err) - return 0 + jww.ERROR.Printf( + "Could not JSON unmarshal response to Receive: %+v", err) + uuidChan <- 0 } - } + uuidChan <- uuid + }) - msgToInsert := buildMessage(messageID.Bytes(), nil, text, pubKey, timestamp, - round.ID, mType, status) - uuid, err := w.receiveHelper(msgToInsert, false) - if err != nil { - jww.ERROR.Printf("Failed to receive Message: %+v", err) + select { + case uuid := <-uuidChan: + return uuid + case <-time.After(indexedDb.ResponseTimeout): + jww.ERROR.Printf("Timed out after %s waiting for response from the "+ + "worker about Receive", indexedDb.ResponseTimeout) } - go w.receivedMessageCB(uuid, pubKey, false) - return uuid + return 0 } func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, round rounds.Round, status dm.Status) uint64 { - parentErr := errors.New("failed to ReceiveText") - - // If there is no extant Conversation, create one. - _, err := indexedDbWorker.Get( - w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) - if err != nil { - if strings.Contains(err.Error(), indexedDbWorker.ErrDoesNotExist) { - err = w.joinConversation(nickname, pubKey, dmToken, codeset) - if err != nil { - jww.ERROR.Printf("%+v", err) - } - } else { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to get Conversation: %+v", err)) - } + msg := TransferMessage{ + MessageID: messageID, + Nickname: nickname, + Text: []byte(text), + PubKey: pubKey, + DmToken: dmToken, + Codeset: codeset, + Timestamp: timestamp, + Round: round, + Status: status, + } + + data, err := json.Marshal(msg) + if err != nil { + jww.ERROR.Printf( + "Could not JSON marshal payload for TransferMessage: %+v", err) return 0 - } else { - jww.DEBUG.Printf("Conversation with %s already joined", nickname) } - // Handle encryption, if it is present - textBytes := []byte(text) - if w.cipher != nil { - textBytes, err = w.cipher.Encrypt(textBytes) + uuidChan := make(chan uint64) + w.wh.SendMessage(indexedDb.ReceiveTextTag, data, func(data []byte) { + var uuid uint64 + err = json.Unmarshal(data, &uuid) if err != nil { - jww.ERROR.Printf("Failed to encrypt Message: %+v", err) - return 0 + jww.ERROR.Printf( + "Could not JSON unmarshal response to ReceiveText: %+v", err) + uuidChan <- 0 } - } + uuidChan <- uuid + }) - msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes, - pubKey, timestamp, round.ID, dm.TextType, status) - - uuid, err := w.receiveHelper(msgToInsert, false) - if err != nil { - jww.ERROR.Printf("Failed to receive Message: %+v", err) + select { + case uuid := <-uuidChan: + return uuid + case <-time.After(indexedDb.ResponseTimeout): + jww.ERROR.Printf("Timed out after %s waiting for response from the "+ + "worker about ReceiveText", indexedDb.ResponseTimeout) } - go w.receivedMessageCB(uuid, pubKey, false) - return uuid + return 0 } func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, round rounds.Round, status dm.Status) uint64 { - parentErr := errors.New("failed to ReceiveReply") - - // If there is no extant Conversation, create one. - _, err := indexedDbWorker.Get( - w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) - if err != nil { - if strings.Contains(err.Error(), indexedDbWorker.ErrDoesNotExist) { - err = w.joinConversation(nickname, pubKey, dmToken, codeset) - if err != nil { - jww.ERROR.Printf("%+v", err) - } - } else { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to get Conversation: %+v", err)) - } + msg := TransferMessage{ + MessageID: messageID, + ReactionTo: reactionTo, + Nickname: nickname, + Text: []byte(text), + PubKey: pubKey, + DmToken: dmToken, + Codeset: codeset, + Timestamp: timestamp, + Round: round, + Status: status, + } + + data, err := json.Marshal(msg) + if err != nil { + jww.ERROR.Printf( + "Could not JSON marshal payload for TransferMessage: %+v", err) return 0 - } else { - jww.DEBUG.Printf("Conversation with %s already joined", nickname) } - // Handle encryption, if it is present - textBytes := []byte(text) - if w.cipher != nil { - textBytes, err = w.cipher.Encrypt(textBytes) + uuidChan := make(chan uint64) + w.wh.SendMessage(indexedDb.ReceiveReplyTag, data, func(data []byte) { + var uuid uint64 + err = json.Unmarshal(data, &uuid) if err != nil { - jww.ERROR.Printf("Failed to encrypt Message: %+v", err) - return 0 + jww.ERROR.Printf( + "Could not JSON unmarshal response to ReceiveReply: %+v", err) + uuidChan <- 0 } - } - - msgToInsert := buildMessage(messageID.Bytes(), reactionTo.Marshal(), textBytes, - pubKey, timestamp, round.ID, dm.TextType, status) + uuidChan <- uuid + }) - uuid, err := w.receiveHelper(msgToInsert, false) - if err != nil { - jww.ERROR.Printf("Failed to receive Message: %+v", err) + select { + case uuid := <-uuidChan: + return uuid + case <-time.After(indexedDb.ResponseTimeout): + jww.ERROR.Printf("Timed out after %s waiting for response from the "+ + "worker about ReceiveReply", indexedDb.ResponseTimeout) } - go w.receivedMessageCB(uuid, pubKey, false) - return uuid + return 0 } -func (w *wasmModel) ReceiveReaction(messageID, _ message.ID, nickname, +func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname, reaction string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, round rounds.Round, status dm.Status) uint64 { - parentErr := errors.New("failed to ReceiveText") - - // If there is no extant Conversation, create one. - _, err := indexedDbWorker.Get( - w.db, conversationStoreName, utils.CopyBytesToJS(pubKey)) - if err != nil { - if strings.Contains(err.Error(), indexedDbWorker.ErrDoesNotExist) { - err = w.joinConversation(nickname, pubKey, dmToken, codeset) - if err != nil { - jww.ERROR.Printf("%+v", err) - } - } else { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to get Conversation: %+v", err)) - } + msg := TransferMessage{ + MessageID: messageID, + ReactionTo: reactionTo, + Nickname: nickname, + Text: []byte(reaction), + PubKey: pubKey, + DmToken: dmToken, + Codeset: codeset, + Timestamp: timestamp, + Round: round, + Status: status, + } + + data, err := json.Marshal(msg) + if err != nil { + jww.ERROR.Printf( + "Could not JSON marshal payload for TransferMessage: %+v", err) return 0 - } else { - jww.DEBUG.Printf("Conversation with %s already joined", nickname) } - // Handle encryption, if it is present - textBytes := []byte(reaction) - if w.cipher != nil { - textBytes, err = w.cipher.Encrypt(textBytes) + uuidChan := make(chan uint64) + w.wh.SendMessage(indexedDb.ReceiveReactionTag, data, func(data []byte) { + var uuid uint64 + err = json.Unmarshal(data, &uuid) if err != nil { - jww.ERROR.Printf("Failed to encrypt Message: %+v", err) - return 0 + jww.ERROR.Printf( + "Could not JSON unmarshal response to ReceiveReaction: %+v", err) + uuidChan <- 0 } - } - - msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes, - pubKey, timestamp, round.ID, dm.ReactionType, status) + uuidChan <- uuid + }) - uuid, err := w.receiveHelper(msgToInsert, false) - if err != nil { - jww.ERROR.Printf("Failed to receive Message: %+v", err) + select { + case uuid := <-uuidChan: + return uuid + case <-time.After(indexedDb.ResponseTimeout): + jww.ERROR.Printf("Timed out after %s waiting for response from the "+ + "worker about ReceiveReaction", indexedDb.ResponseTimeout) } - go w.receivedMessageCB(uuid, pubKey, false) - return uuid + return 0 } func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID, timestamp time.Time, round rounds.Round, status dm.Status) { - parentErr := errors.New("failed to UpdateSentStatus") - - // FIXME: this is a bit of race condition without the mux. - // This should be done via the transactions (i.e., make a - // special version of receiveHelper) - w.updateMux.Lock() - defer w.updateMux.Unlock() - - // Convert messageID to the key generated by json.Marshal - key := js.ValueOf(uuid) - - // Use the key to get the existing Message - currentMsg, err := indexedDbWorker.Get(w.db, messageStoreName, key) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to get message: %+v", err)) - return - } - - // Extract the existing Message and update the Status - newMessage := &Message{} - err = json.Unmarshal([]byte(utils.JsToJson(currentMsg)), newMessage) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Could not JSON unmarshal message: %+v", err)) - return - } - - newMessage.Status = uint8(status) - if !messageID.Equals(message.ID{}) { - newMessage.MessageID = messageID.Bytes() - } - - if round.ID != 0 { - newMessage.Round = uint64(round.ID) - } - - if !timestamp.Equal(time.Time{}) { - newMessage.Timestamp = timestamp - } - - // Store the updated Message - _, err = w.receiveHelper(newMessage, true) - if err != nil { - jww.ERROR.Printf("%+v", errors.Wrap(parentErr, err.Error())) - } - go w.receivedMessageCB(uuid, newMessage.ConversationPubKey, true) -} - -// receiveHelper is a private helper for receiving any sort of message. -func (w *wasmModel) receiveHelper( - newMessage *Message, isUpdate bool) (uint64, error) { - // Convert to jsObject - newMessageJson, err := json.Marshal(newMessage) - if err != nil { - return 0, errors.Errorf("Unable to marshal Message: %+v", err) - } - messageObj, err := utils.JsonToJS(newMessageJson) - if err != nil { - return 0, errors.Errorf("Unable to marshal Message: %+v", err) - } - - // Unset the primaryKey for inserts so that it can be auto-populated and - // incremented - if !isUpdate { - messageObj.Delete("id") + msg := TransferMessage{ + UUID: uuid, + MessageID: messageID, + Timestamp: timestamp, + Round: round, + Status: status, } - // Store message to database - result, err := indexedDbWorker.Put(w.db, messageStoreName, messageObj) + data, err := json.Marshal(msg) if err != nil { - return 0, errors.Errorf("Unable to put Message: %+v", err) + jww.ERROR.Printf( + "Could not JSON marshal payload for TransferMessage: %+v", err) } - // NOTE: Sometimes the insert fails to return an error but hits a duplicate - // insert, so this fallthrough returns the UUID entry in that case. - if result.IsUndefined() { - msgID := message.ID{} - copy(msgID[:], newMessage.MessageID) - uuid, errLookup := w.msgIDLookup(msgID) - if uuid != 0 && errLookup == nil { - return uuid, nil - } - return 0, errors.Errorf("uuid lookup failure: %+v", err) - } - uuid := uint64(result.Int()) - jww.DEBUG.Printf("Successfully stored message %d", uuid) - - return uuid, nil -} - -// msgIDLookup gets the UUID of the Message with the given messageID. -func (w *wasmModel) msgIDLookup(messageID message.ID) (uint64, error) { - resultObj, err := indexedDbWorker.GetIndex(w.db, messageStoreName, - messageStoreMessageIndex, utils.CopyBytesToJS(messageID.Marshal())) - if err != nil { - return 0, err - } - - uuid := uint64(0) - if !resultObj.IsUndefined() { - uuid = uint64(resultObj.Get("id").Int()) - } - return uuid, nil + w.wh.SendMessage(indexedDb.UpdateSentStatusTag, data, nil) } diff --git a/indexedDbWorker/dm/init.go b/indexedDbWorker/dm/init.go index 184f81311a20f4c0e5f0cfe7a802cd8df4d1bed3..269d7d135ca87331516c364ef2983240d076e552 100644 --- a/indexedDbWorker/dm/init.go +++ b/indexedDbWorker/dm/init.go @@ -7,31 +7,24 @@ //go:build js && wasm -package main +package channelEventModel import ( "crypto/ed25519" - "gitlab.com/elixxir/xxdk-wasm/indexedDb" - "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" - "syscall/js" + "encoding/json" "time" - "github.com/hack-pad/go-indexeddb/idb" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/dm" cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker" + "gitlab.com/elixxir/xxdk-wasm/storage" ) -const ( - // databaseSuffix is the suffix to be appended to the name of - // the database. - databaseSuffix = "_speakeasy_dm" - - // currentVersion is the current version of the IndexDb - // runtime. Used for migration purposes. - currentVersion uint = 1 -) +// WorkerJavascriptFileURL is the URL of the script the worker will execute to +// launch the worker WASM binary. It must obey the same-origin policy. +const WorkerJavascriptFileURL = "/integrations/assets/indexedDbWorker.js" // MessageReceivedCallback is called any time a message is received or updated. // @@ -39,192 +32,134 @@ const ( type MessageReceivedCallback func( uuid uint64, pubKey ed25519.PublicKey, update bool) +// NewWASMEventModelMessage is JSON marshalled and sent to the worker for +// [NewWASMEventModel]. +type NewWASMEventModelMessage struct { + Path string `json:"path"` + EncryptionJSON string `json:"encryptionJSON"` +} + // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel. // The name should be a base64 encoding of the users public key. func NewWASMEventModel(path string, encryption cryptoChannel.Cipher, - cb MessageReceivedCallback, storeEncryptionStatus storeEncryptionStatusFn) ( - dm.EventModel, error) { - databaseName := path + databaseSuffix - return newWASMModel(databaseName, encryption, cb, storeEncryptionStatus) -} + cb MessageReceivedCallback) (dm.EventModel, error) { -// storeEncryptionStatusFn matches storage.StoreIndexedDbEncryptionStatus so -// that the data can be sent between the worker and main thread. -type storeEncryptionStatusFn func( - databaseName string, encryptionStatus bool) (bool, error) - -// newWASMModel creates the given [idb.Database] and returns a wasmModel. -func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, - cb MessageReceivedCallback, storeEncryptionStatus storeEncryptionStatusFn) ( - *wasmModel, error) { - // Attempt to open database object - ctx, cancel := indexedDbWorker.NewContext() - defer cancel() - openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion, - func(db *idb.Database, oldVersion, newVersion uint) error { - if oldVersion == newVersion { - jww.INFO.Printf("IndexDb version is current: v%d", - newVersion) - return nil - } - - jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d", - oldVersion, newVersion) - - if oldVersion == 0 && newVersion >= 1 { - err := v1Upgrade(db) - if err != nil { - return err - } - oldVersion = 1 - } - - // if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 } - return nil - }) + // TODO: bring in URL and name from caller + wh, err := indexedDb.NewWorkerHandler( + WorkerJavascriptFileURL, "dmIndexedDb") if err != nil { return nil, err } - // Wait for database open to finish - db, err := openRequest.Await(ctx) - if err != nil { - return nil, err - } + // Register handler to manage messages for the MessageReceivedCallback + wh.RegisterHandler(indexedDb.GetMessageTag, indexedDb.InitID, false, + messageReceivedCallbackHandler(cb)) + + // Register handler to manage checking encryption status from local storage + wh.RegisterHandler(indexedDb.EncryptionStatusTag, indexedDb.InitID, false, + checkDbEncryptionStatusHandler(wh)) - // Save the encryption status to storage - encryptionStatus := encryption != nil - loadedEncryptionStatus, err := - storeEncryptionStatus(databaseName, encryptionStatus) + encryptionJSON, err := json.Marshal(encryption) if err != nil { return nil, err } - // Verify encryption status does not change - if encryptionStatus != loadedEncryptionStatus { - return nil, errors.New( - "Cannot load database with different encryption status.") - } else if !encryptionStatus { - jww.WARN.Printf("IndexedDb encryption disabled!") + message := NewWASMEventModelMessage{ + Path: path, + EncryptionJSON: string(encryptionJSON), } - // Attempt to ensure the database has been properly initialized - openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion, - func(db *idb.Database, oldVersion, newVersion uint) error { - return nil - }) + payload, err := json.Marshal(message) if err != nil { return nil, err } - // Wait for database open to finish - db, err = openRequest.Await(ctx) - if err != nil { - return nil, err - } - wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption} - return wrapper, nil -} + errChan := make(chan string) + wh.SendMessage(indexedDb.NewWASMEventModelTag, payload, func(data []byte) { + errChan <- string(data) + }) -// v1Upgrade performs the v0 -> v1 database upgrade. -// -// This can never be changed without permanently breaking backwards -// compatibility. -func v1Upgrade(db *idb.Database) error { - indexOpts := idb.IndexOptions{ - Unique: false, - MultiEntry: false, + select { + case workerErr := <-errChan: + if workerErr != "" { + return nil, errors.New(workerErr) + } + case <-time.After(indexedDb.ResponseTimeout): + return nil, errors.Errorf("timed out after %s waiting for indexedDB "+ + "database in worker to intialize", indexedDb.ResponseTimeout) } - // Build Message ObjectStore and Indexes - messageStoreOpts := idb.ObjectStoreOptions{ - KeyPath: js.ValueOf(msgPkeyName), - AutoIncrement: true, - } - messageStore, err := db.CreateObjectStore(messageStoreName, messageStoreOpts) - if err != nil { - return err - } - _, err = messageStore.CreateIndex(messageStoreMessageIndex, - js.ValueOf(messageStoreMessage), - idb.IndexOptions{ - Unique: true, - MultiEntry: false, - }) - if err != nil { - return err - } - _, err = messageStore.CreateIndex(messageStoreConversationIndex, - js.ValueOf(messageStoreConversation), indexOpts) - if err != nil { - return err - } - _, err = messageStore.CreateIndex(messageStoreParentIndex, - js.ValueOf(messageStoreParent), indexOpts) - if err != nil { - return err - } - _, err = messageStore.CreateIndex(messageStoreTimestampIndex, - js.ValueOf(messageStoreTimestamp), indexOpts) - if err != nil { - return err - } + return &wasmModel{wh}, nil +} - // Build Channel ObjectStore - conversationStoreOpts := idb.ObjectStoreOptions{ - KeyPath: js.ValueOf(convoPkeyName), - AutoIncrement: false, - } - _, err = db.CreateObjectStore(conversationStoreName, conversationStoreOpts) - if err != nil { - return err - } +// MessageReceivedCallbackMessage is JSON marshalled and received from the +// worker for the [MessageReceivedCallback] callback. +type MessageReceivedCallbackMessage struct { + UUID uint64 `json:"uuid"` + PubKey ed25519.PublicKey `json:"pubKey"` + Update bool `json:"update"` +} - // Get the database name and save it to storage - if databaseName, err2 := db.Name(); err2 != nil { - return err2 - } else if err = storeDatabaseName(databaseName); err != nil { - return err +// messageReceivedCallbackHandler returns a handler to manage messages for the +// MessageReceivedCallback. +func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte) { + return func(data []byte) { + var msg MessageReceivedCallbackMessage + err := json.Unmarshal(data, &msg) + if err != nil { + jww.ERROR.Printf("Failed to JSON unmarshal "+ + "MessageReceivedCallback message from worker: %+v", err) + return + } + cb(msg.UUID, msg.PubKey, msg.Update) } +} - return nil +// EncryptionStatusMessage is JSON marshalled and received from the worker when +// the database checks if it is encrypted. +type EncryptionStatusMessage struct { + DatabaseName string `json:"databaseName"` + EncryptionStatus bool `json:"encryptionStatus"` } -// storeDatabaseName sends the database name to storage.StoreIndexedDb in the -// main thread to be stored in localstorage and waits for the error to be -// returned. -// -// The function specified below is a placeholder until set by -// registerDatabaseNameStore. registerDatabaseNameStore must be called before -// storeDatabaseName. -var storeDatabaseName = func(databaseName string) error { return nil } - -// RegisterDatabaseNameStore sets storeDatabaseName to send the database to -// storage.StoreIndexedDb in the main thread when called and registers a handler -// to listen for the response. -func RegisterDatabaseNameStore(m *manager) { - storeDatabaseNameResponseChan := make(chan []byte) - // Register handler - m.mh.RegisterHandler(indexedDb.StoreDatabaseNameTag, func(data []byte) []byte { - storeDatabaseNameResponseChan <- data - return nil - }) +// EncryptionStatusReply is JSON marshalled and sent to the worker is response +// to the [EncryptionStatusMessage]. +type EncryptionStatusReply struct { + EncryptionStatus bool `json:"encryptionStatus"` + Error string `json:"error"` +} - storeDatabaseName = func(databaseName string) error { - m.mh.SendResponse(indexedDb.StoreDatabaseNameTag, indexedDb.InitID, - []byte(databaseName)) - - // Wait for response - select { - case response := <-storeDatabaseNameResponseChan: - if len(response) > 0 { - return errors.New(string(response)) - } - case <-time.After(indexedDb.ResponseTimeout): - return errors.Errorf("timed out after %s waiting for "+ - "response about storing the database name in local "+ - "storage in the main thread", indexedDb.ResponseTimeout) +// checkDbEncryptionStatusHandler returns a handler to manage checking +// encryption status from local storage. +func checkDbEncryptionStatusHandler(wh *indexedDb.WorkerHandler) func(data []byte) { + return func(data []byte) { + // Unmarshal received message + var msg EncryptionStatusMessage + err := json.Unmarshal(data, &msg) + if err != nil { + jww.ERROR.Printf("Failed to JSON unmarshal "+ + "EncryptionStatusMessage message from worker: %+v", err) + return } - return nil + + // Pass message values to storage + loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( + msg.DatabaseName, msg.EncryptionStatus) + var reply EncryptionStatusReply + if err != nil { + reply.Error = err.Error() + } else { + reply.EncryptionStatus = loadedEncryptionStatus + } + + // Return response + statusData, err := json.Marshal(reply) + if err != nil { + jww.ERROR.Printf( + "Failed to JSON marshal EncryptionStatusReply: %+v", err) + return + } + + wh.SendMessage(indexedDb.EncryptionStatusTag, statusData, nil) } } diff --git a/indexedDb/tag.go b/indexedDbWorker/tag.go similarity index 100% rename from indexedDb/tag.go rename to indexedDbWorker/tag.go diff --git a/indexedDb/worker.go b/indexedDbWorker/worker.go similarity index 100% rename from indexedDb/worker.go rename to indexedDbWorker/worker.go diff --git a/wasm/channels.go b/wasm/channels.go index d32737321c0efbab9ff732917b185a7b2cfbea99..5fa1edced65da86700f88414a6b4d04e0c91aa7b 100644 --- a/wasm/channels.go +++ b/wasm/channels.go @@ -14,7 +14,7 @@ import ( "encoding/json" "errors" "gitlab.com/elixxir/client/v4/channels" - channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/channels" + channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker/channels" "gitlab.com/xx_network/primitives/id" "sync" "syscall/js" @@ -1832,7 +1832,7 @@ func (em *eventModel) GetMessage(messageID []byte) ([]byte, error) { // the database. // // Parameters: -// - messageID - The bytes of the [channel.MessageID] of the message. +// - messageID - The bytes of the [channel.MessageID] of the message. func (em *eventModel) DeleteMessage(messageID []byte) error { err := em.deleteMessage(utils.CopyBytesToJS(messageID)) if !err.IsUndefined() { diff --git a/wasm/dm.go b/wasm/dm.go index 678ca74f529f1ccce1a0a8cac63242b5073ed835..613835eb6490fc77e1189bc213c57b39c71f9c88 100644 --- a/wasm/dm.go +++ b/wasm/dm.go @@ -13,7 +13,7 @@ import ( "crypto/ed25519" "syscall/js" - indexDB "gitlab.com/elixxir/xxdk-wasm/indexedDb/dm" + indexDB "gitlab.com/elixxir/xxdk-wasm/indexedDbWorker/dm" "encoding/base64" @@ -129,7 +129,7 @@ func NewDMClient(_ js.Value, args []js.Value) any { } // NewDMClientWithIndexedDb creates a new [DMClient] from a new -// private identity ([channel.PrivateIdentity]) and using indexedDb as a backend +// private identity ([channel.PrivateIdentity]) and using indexedDbWorker as a backend // to manage the event model. // // This is for creating a manager for an identity for the first time. For @@ -137,7 +137,7 @@ func NewDMClient(_ js.Value, args []js.Value) any { // reload this channel manager, use [LoadDMClientWithIndexedDb], passing // in the storage tag retrieved by [DMClient.GetStorageTag]. // -// This function initialises an indexedDb database. +// This function initialises an indexedDbWorker database. // // Parameters: // - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved @@ -145,7 +145,7 @@ func NewDMClient(_ js.Value, args []js.Value) any { // - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is // generated by [GenerateChannelIdentity] (Uint8Array). // - args[2] - Function that takes in the same parameters as -// [indexedDb.MessageReceivedCallback]. On the Javascript side, the UUID is +// [indexedDbWorker.MessageReceivedCallback]. On the Javascript side, the UUID is // returned as an int and the channelID as a Uint8Array. The row in the // database that was updated can be found using the UUID. The channel ID is // provided so that the recipient can filter if they want to the processes @@ -157,7 +157,7 @@ func NewDMClient(_ js.Value, args []js.Value) any { // // Returns a promise: // - Resolves to a Javascript representation of the [DMClient] object. -// - Rejected with an error if loading indexedDb or the manager fails. +// - Rejected with an error if loading indexedDbWorker or the manager fails. // - Throws a TypeError if the cipher ID does not correspond to a cipher. func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any { cmixID := args[0].Int() @@ -175,7 +175,7 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any { } // NewDMClientWithIndexedDbUnsafe creates a new [DMClient] from a -// new private identity ([channel.PrivateIdentity]) and using indexedDb as a +// new private identity ([channel.PrivateIdentity]) and using indexedDbWorker as a // backend to manage the event model. However, the data is written in plain text // and not encrypted. It is recommended that you do not use this in production. // @@ -184,7 +184,7 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any { // reload this channel manager, use [LoadDMClientWithIndexedDbUnsafe], // passing in the storage tag retrieved by [DMClient.GetStorageTag]. // -// This function initialises an indexedDb database. +// This function initialises an indexedDbWorker database. // // Parameters: // - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved @@ -192,7 +192,7 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any { // - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is // generated by [GenerateChannelIdentity] (Uint8Array). // - args[2] - Function that takes in the same parameters as -// [indexedDb.MessageReceivedCallback]. On the Javascript side, the UUID is +// [indexedDbWorker.MessageReceivedCallback]. On the Javascript side, the UUID is // returned as an int and the channelID as a Uint8Array. The row in the // database that was updated can be found using the UUID. The channel ID is // provided so that the recipient can filter if they want to the processes @@ -201,7 +201,7 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any { // // Returns a promise: // - Resolves to a Javascript representation of the [DMClient] object. -// - Rejected with an error if loading indexedDb or the manager fails. +// - Rejected with an error if loading indexedDbWorker or the manager fails. func NewDMClientWithIndexedDbUnsafe(_ js.Value, args []js.Value) any { cmixID := args[0].Int() privateIdentity := utils.CopyBytesToGo(args[1])