diff --git a/go.mod b/go.mod index d87eb1fcf27a77311d89d041a171f392b436fc6a..39de981529fe2fb13dfab43bc9ecff8d9e56c07c 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,11 @@ require ( github.com/hack-pad/go-indexeddb v0.2.0 github.com/pkg/errors v0.9.1 github.com/spf13/jwalterweatherman v1.1.0 - gitlab.com/elixxir/client/v4 v4.3.9-0.20221208215024-325fafebf519 - gitlab.com/elixxir/crypto v0.0.7-0.20221208214832-13e2a751db1a + gitlab.com/elixxir/client/v4 v4.3.9-0.20221210003613-b73478d56e0d + gitlab.com/elixxir/crypto v0.0.7-0.20221210003748-5187f4b98788 gitlab.com/elixxir/primitives v0.0.3-0.20221114231218-cc461261a6af gitlab.com/xx_network/crypto v0.0.5-0.20221121220724-8eefdbb0eb46 - gitlab.com/xx_network/primitives v0.0.4-0.20221110180011-fd6ea3058225 + gitlab.com/xx_network/primitives v0.0.4-0.20221209210320-376735467d58 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa ) diff --git a/go.sum b/go.sum index 3f001c22436716a340a99a493d19c407914cde78..afdff6c2a0129b4c7613853930e4f79ac7722697 100644 --- a/go.sum +++ b/go.sum @@ -378,12 +378,12 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f h1:yXGvNBqzZwAhDYlSnxPRbgor6JWoOt1Z7s3z1O9JR40= gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k= -gitlab.com/elixxir/client/v4 v4.3.9-0.20221208215024-325fafebf519 h1:nxGzvotSXP/7ekAWj0VpHcieXzxV/wxJ6DD4KG8aid8= -gitlab.com/elixxir/client/v4 v4.3.9-0.20221208215024-325fafebf519/go.mod h1:ID5txokZGTr7l+xTAoEtQMSSdvNZUBntJAWTR81upds= +gitlab.com/elixxir/client/v4 v4.3.9-0.20221210003613-b73478d56e0d h1:Ydy9DnxHrrCfHY2UI6//88wT9L2kKtXUuA6di7ER3Ew= +gitlab.com/elixxir/client/v4 v4.3.9-0.20221210003613-b73478d56e0d/go.mod h1:76yQ2oAQgAIFsb71+sZXeb361RBEfOU7jjY8gtyXTgU= gitlab.com/elixxir/comms v0.0.4-0.20221110181420-84bca6216fe4 h1:bLRjVCyMVde4n2hTVgoyyIAWrKI4CevpChchkPeb6A0= gitlab.com/elixxir/comms v0.0.4-0.20221110181420-84bca6216fe4/go.mod h1:XhI2/CMng+xcH3mAs+1aPz29PSNu1079XMJ8V+xxihw= -gitlab.com/elixxir/crypto v0.0.7-0.20221208214832-13e2a751db1a h1:d514iJOaPmH2qjqUyI1N93UyEPTWvZ40LJiRPvQ89jw= -gitlab.com/elixxir/crypto v0.0.7-0.20221208214832-13e2a751db1a/go.mod h1:fb6UMdmr0hVnzOU67hOZzTeS+wcQZ4pUtTO82039wGg= +gitlab.com/elixxir/crypto v0.0.7-0.20221210003748-5187f4b98788 h1:K0kDn2k54rwwXx8Cay/0OysfE1AI052E0dfAxOlkamQ= +gitlab.com/elixxir/crypto v0.0.7-0.20221210003748-5187f4b98788/go.mod h1:fb6UMdmr0hVnzOU67hOZzTeS+wcQZ4pUtTO82039wGg= gitlab.com/elixxir/ekv v0.2.1 h1:dtwbt6KmAXG2Tik5d60iDz2fLhoFBgWwST03p7T+9Is= gitlab.com/elixxir/ekv v0.2.1/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU= gitlab.com/elixxir/primitives v0.0.3-0.20221114231218-cc461261a6af h1:xcPqknK1ehNb9xwcutTdoR0YgD7DC/ySh9z49tIpSxQ= @@ -392,8 +392,8 @@ gitlab.com/xx_network/comms v0.0.4-0.20221207203143-462f82d6ec01 h1:0jkud7OWqneH gitlab.com/xx_network/comms v0.0.4-0.20221207203143-462f82d6ec01/go.mod h1:+RfHgk75ywMvmucOpPS7rSUlsnbPyBuLsr13tsthUTE= gitlab.com/xx_network/crypto v0.0.5-0.20221121220724-8eefdbb0eb46 h1:6AHgUpWdJ72RVTTdJSvfThZiYTQNUnrPaTCl/EkRLpg= gitlab.com/xx_network/crypto v0.0.5-0.20221121220724-8eefdbb0eb46/go.mod h1:acWUBKCpae/XVaQF7J9RnLAlBT13i5r7gnON+mrIxBk= -gitlab.com/xx_network/primitives v0.0.4-0.20221110180011-fd6ea3058225 h1:TAn87e6Zt9KwcSnWKyIul5eu8T0RHY9FDubCGs3G0dw= -gitlab.com/xx_network/primitives v0.0.4-0.20221110180011-fd6ea3058225/go.mod h1:rP/2IsqIFHapuIB4mstXKItvwoJRQ9Wlms/NGeutHsk= +gitlab.com/xx_network/primitives v0.0.4-0.20221209210320-376735467d58 h1:HpeUIf1gIIelLH3LHxEf3/GalecbbtZnOnIegJHALoc= +gitlab.com/xx_network/primitives v0.0.4-0.20221209210320-376735467d58/go.mod h1:wUxbEBGOBJZ/RkAiVAltlC1uIlIrU0dE113Nq7HiOhw= gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981 h1:1s0vX9BbkiD0IVXwr3LOaTBcq1wBrWcUWMBK0s8r0Z0= gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981/go.mod h1:aLzpP2TiZTQut/PVHR40EJAomzugDdHXetbieRClXIM= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= diff --git a/indexedDb/implementation.go b/indexedDb/channels/implementation.go similarity index 67% rename from indexedDb/implementation.go rename to indexedDb/channels/implementation.go index d819fde24d9d9404e3f23fb20d7376594eb07278..36c5980921de39d694c1ab0b0df3ecb702bb439c 100644 --- a/indexedDb/implementation.go +++ b/indexedDb/channels/implementation.go @@ -7,13 +7,13 @@ //go:build js && wasm -package indexedDb +package channels import ( - "context" "crypto/ed25519" "encoding/base64" "encoding/json" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" "sync" "syscall/js" "time" @@ -30,10 +30,6 @@ import ( "gitlab.com/xx_network/primitives/id" ) -// dbTimeout is the global timeout for operations with the storage -// [context.Context]. -const dbTimeout = time.Second - // wasmModel implements [channels.EventModel] interface, which uses the channels // system passed an object that adheres to in order to get events on the // channel. @@ -44,11 +40,6 @@ type wasmModel struct { updateMux sync.Mutex } -// newContext builds a context for database operations. -func newContext() (context.Context, context.CancelFunc) { - return context.WithTimeout(context.Background(), dbTimeout) -} - // JoinChannel is called whenever a channel is joined locally. func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { parentErr := errors.New("failed to JoinChannel") @@ -74,38 +65,11 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { return } - // Prepare the Transaction - txn, err := w.db.Transaction(idb.TransactionReadWrite, channelsStoreName) + _, err = indexedDb.Put(w.db, channelsStoreName, channelObj) if err != nil { jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to create Transaction: %+v", err)) - return - } - store, err := txn.ObjectStore(channelsStoreName) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to get ObjectStore: %+v", err)) - return + "Unable to put Channel: %+v", err)) } - - // Perform the operation - _, err = store.Put(channelObj) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Unable to Add Channel: %+v", err)) - return - } - - // Wait for the operation to return - ctx, cancel := newContext() - err = txn.Await(ctx) - cancel() - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Adding Channel failed: %+v", err)) - return - } - jww.DEBUG.Printf("Successfully added channel: %s", channel.ReceptionID) } // LeaveChannel is called whenever a channel is left locally. @@ -135,7 +99,7 @@ func (w *wasmModel) LeaveChannel(channelID *id.ID) { } // Wait for the operation to return - ctx, cancel := newContext() + ctx, cancel := indexedDb.NewContext() err = txn.Await(ctx) cancel() if err != nil { @@ -183,7 +147,7 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error { if err != nil { return errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err) } - ctx, cancel := newContext() + ctx, cancel := indexedDb.NewContext() err = cursorRequest.Iter(ctx, func(cursor *idb.CursorWithValue) error { _, err := cursor.Delete() @@ -203,7 +167,7 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error { // user of the API to filter such called by message ID. func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID cryptoChannel.MessageID, nickname, text string, - pubKey ed25519.PublicKey, dmToken []byte, codeset uint8, + pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus) uint64 { textBytes := []byte(text) @@ -239,7 +203,7 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, // the initial message. As a result, it may be important to buffer replies. func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID cryptoChannel.MessageID, replyTo cryptoChannel.MessageID, - nickname, text string, pubKey ed25519.PublicKey, dmToken []byte, codeset uint8, + 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) uint64 { textBytes := []byte(text) @@ -275,7 +239,7 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, // the initial message. As a result, it may be important to buffer reactions. func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID, - nickname, reaction string, pubKey ed25519.PublicKey, dmToken []byte, codeset uint8, + nickname, reaction string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus) uint64 { textBytes := []byte(reaction) @@ -322,7 +286,7 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, key := js.ValueOf(uuid) // Use the key to get the existing Message - currentMsg, err := w.get(messageStoreName, key) + currentMsg, err := indexedDb.Get(w.db, messageStoreName, key) if err != nil { return } @@ -363,7 +327,7 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, // 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 []byte, codeset uint8, timestamp time.Time, + text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round id.Round, mType channels.MessageType, status channels.SentStatus) *Message { return &Message{ @@ -405,31 +369,15 @@ func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64, messageObj.Delete("id") } - // Prepare the Transaction - txn, err := w.db.Transaction(idb.TransactionReadWrite, messageStoreName) - if err != nil { - return 0, errors.Errorf("Unable to create Transaction: %+v", - err) - } - store, err := txn.ObjectStore(messageStoreName) - if err != nil { - return 0, errors.Errorf("Unable to get ObjectStore: %+v", err) - } - - // Perform the upsert (put) operation - addReq, err := store.Put(messageObj) + // Store message to database + addReq, err := indexedDb.Put(w.db, messageStoreName, messageObj) if err != nil { - return 0, errors.Errorf("Unable to upsert Message: %+v", err) + return 0, errors.Errorf("Unable to put Message: %+v", err) } - - // Wait for the operation to return - ctx, cancel := newContext() - err = txn.Await(ctx) - cancel() + res, err := addReq.Result() if err != nil { - return 0, errors.Errorf("Upserting Message failed: %+v", err) + return 0, errors.Errorf("Unable to get Message result: %+v", err) } - res, err := addReq.Result() // NOTE: Sometimes the insert fails to return an error but hits a duplicate // insert, so this fallthrough returns the UUID entry in that case. @@ -448,135 +396,19 @@ func (w *wasmModel) receiveHelper(newMessage *Message, isUpdate bool) (uint64, return uuid, nil } -// get is a generic private helper for getting values from the given -// [idb.ObjectStore]. -func (w *wasmModel) get(objectStoreName string, key js.Value) (string, error) { - parentErr := errors.Errorf("failed to get %s/%s", objectStoreName, key) - - // Prepare the Transaction - txn, err := w.db.Transaction(idb.TransactionReadOnly, objectStoreName) - if err != nil { - return "", errors.WithMessagef(parentErr, - "Unable to create Transaction: %+v", err) - } - store, err := txn.ObjectStore(objectStoreName) - if err != nil { - return "", errors.WithMessagef(parentErr, - "Unable to get ObjectStore: %+v", err) - } - - // Perform the operation - getRequest, err := store.Get(key) - if err != nil { - return "", errors.WithMessagef(parentErr, - "Unable to Get from ObjectStore: %+v", err) - } - - // Wait for the operation to return - ctx, cancel := newContext() - resultObj, err := getRequest.Await(ctx) - cancel() - if err != nil { - return "", errors.WithMessagef(parentErr, - "Unable to get from ObjectStore: %+v", err) - } - - // Process result into string - resultStr := utils.JsToJson(resultObj) - jww.DEBUG.Printf("Got from %s/%s: %s", objectStoreName, key, resultStr) - return resultStr, nil -} - +// msgIDLookup gets the UUID of the Message with the given messageID. func (w *wasmModel) msgIDLookup(messageID cryptoChannel.MessageID) (uint64, error) { - parentErr := errors.Errorf("failed to get %s/%s", messageStoreName, - messageID) - - // Prepare the Transaction - txn, err := w.db.Transaction(idb.TransactionReadOnly, messageStoreName) - if err != nil { - return 0, errors.WithMessagef(parentErr, - "Unable to create Transaction: %+v", err) - } - store, err := txn.ObjectStore(messageStoreName) - if err != nil { - return 0, errors.WithMessagef(parentErr, - "Unable to get ObjectStore: %+v", err) - } - idx, err := store.Index(messageStoreMessageIndex) - if err != nil { - return 0, errors.WithMessagef(parentErr, - "Unable to get index: %+v", err) - } - - msgIDStr := base64.StdEncoding.EncodeToString(messageID.Bytes()) - - keyReq, err := idx.Get(js.ValueOf(msgIDStr)) - if err != nil { - return 0, errors.WithMessagef(parentErr, - "Unable to get keyReq: %+v", err) - } - // Wait for the operation to return - ctx, cancel := newContext() - keyObj, err := keyReq.Await(ctx) - cancel() + msgIDStr := js.ValueOf(base64.StdEncoding.EncodeToString(messageID.Bytes())) + resultObj, err := indexedDb.GetIndex(w.db, messageStoreName, + messageStoreMessageIndex, msgIDStr) if err != nil { - return 0, errors.WithMessagef(parentErr, - "Unable to get from ObjectStore: %+v", err) + return 0, err } - // Process result into string - resultStr := utils.JsToJson(keyObj) - jww.DEBUG.Printf("Index lookup of %s/%s/%s: %s", messageStoreName, - messageStoreMessageIndex, msgIDStr, resultStr) - uuid := uint64(0) - if !keyObj.IsUndefined() { - uuid = uint64(keyObj.Get("id").Int()) + if !resultObj.IsUndefined() { + uuid = uint64(resultObj.Get("id").Int()) } return uuid, nil } - -// dump returns the given [idb.ObjectStore] contents to string slice for -// debugging purposes. -func (w *wasmModel) dump(objectStoreName string) ([]string, error) { - parentErr := errors.Errorf("failed to dump %s", objectStoreName) - - txn, err := w.db.Transaction(idb.TransactionReadOnly, objectStoreName) - if err != nil { - return nil, errors.WithMessagef(parentErr, - "Unable to create Transaction: %+v", err) - } - store, err := txn.ObjectStore(objectStoreName) - if err != nil { - return nil, errors.WithMessagef(parentErr, - "Unable to get ObjectStore: %+v", err) - } - cursorRequest, err := store.OpenCursor(idb.CursorNext) - if err != nil { - return nil, errors.WithMessagef(parentErr, - "Unable to open Cursor: %+v", err) - } - - // Run the query - jww.DEBUG.Printf("%s values:", objectStoreName) - results := make([]string, 0) - ctx, cancel := newContext() - err = cursorRequest.Iter(ctx, - func(cursor *idb.CursorWithValue) error { - value, err := cursor.Value() - if err != nil { - return err - } - valueStr := utils.JsToJson(value) - results = append(results, valueStr) - jww.DEBUG.Printf("- %v", valueStr) - return nil - }) - cancel() - if err != nil { - return nil, errors.WithMessagef(parentErr, - "Unable to dump ObjectStore: %+v", err) - } - return results, nil -} diff --git a/indexedDb/implementation_test.go b/indexedDb/channels/implementation_test.go similarity index 89% rename from indexedDb/implementation_test.go rename to indexedDb/channels/implementation_test.go index 511a022b6c6257f6e8c5877c7be914addedd725d..a14de4f928575189915a04d84a634d160e44d1d8 100644 --- a/indexedDb/implementation_test.go +++ b/indexedDb/channels/implementation_test.go @@ -7,12 +7,13 @@ //go:build js && wasm -package indexedDb +package channels import ( "encoding/json" "fmt" "github.com/hack-pad/go-indexeddb/idb" + "gitlab.com/elixxir/xxdk-wasm/indexedDb" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/xx_network/primitives/netTime" "os" @@ -46,7 +47,7 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { // Store a test message testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, nil, 0, netTime.Now(), + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, channels.Sent) uuid, err := eventModel.receiveHelper(testMsg, false) if err != nil { @@ -54,7 +55,7 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { } // Ensure one message is stored - results, err := eventModel.dump(messageStoreName) + results, err := indexedDb.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -68,7 +69,7 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { rounds.Round{ID: 8675309}, expectedStatus) // Check the resulting status - results, err = eventModel.dump(messageStoreName) + results, err = indexedDb.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -112,7 +113,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { } eventModel.JoinChannel(testChannel) eventModel.JoinChannel(testChannel2) - results, err := eventModel.dump(channelsStoreName) + results, err := indexedDb.Dump(eventModel.db, channelsStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -120,7 +121,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { t.Fatalf("Expected 2 channels to exist") } eventModel.LeaveChannel(testChannel.ReceptionID) - results, err = eventModel.dump(channelsStoreName) + results, err = indexedDb.Dump(eventModel.db, channelsStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -146,13 +147,11 @@ func Test_wasmModel_UUIDTest(t *testing.T) { copy(msgID[:], testString+fmt.Sprintf("%d", i)) rnd := rounds.Round{ID: id.Round(42)} uuid := eventModel.ReceiveMessage(channelID, msgID, "test", - testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, nil, 0, + testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Hour, rnd, 0, channels.Sent) uuids[i] = uuid } - _, _ = eventModel.dump(messageStoreName) - for i := 0; i < 10; i++ { for j := i + 1; j < 10; j++ { if uuids[i] == uuids[j] { @@ -181,13 +180,11 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) { channelID := id.NewIdFromBytes([]byte(testString), t) rnd := rounds.Round{ID: id.Round(42)} uuid := eventModel.ReceiveMessage(channelID, msgID, "test", - testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, nil, 0, + testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Hour, rnd, 0, channels.Sent) uuids[i] = uuid } - _, _ = eventModel.dump(messageStoreName) - for i := 0; i < 10; i++ { for j := i + 1; j < 10; j++ { if uuids[i] != uuids[j] { @@ -225,12 +222,12 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { testMsgId := channel.MakeMessageID([]byte(testStr), &id.ID{1}) eventModel.ReceiveMessage(thisChannel, testMsgId, testStr, testStr, - []byte{8, 6, 7, 5}, nil, 0, netTime.Now(), time.Second, + []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, rounds.Round{ID: id.Round(0)}, 0, channels.Sent) } // Check pre-results - result, err := eventModel.dump(messageStoreName) + result, err := indexedDb.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -245,7 +242,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { } // Check final results - result, err = eventModel.dump(messageStoreName) + result, err = indexedDb.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -286,7 +283,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { // First message insert should succeed testMsgId := channel.MakeMessageID([]byte(testString), &id.ID{1}) testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, nil, 0, netTime.Now(), + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, channels.Sent) _, err = eventModel.receiveHelper(testMsg, false) if err != nil { @@ -298,7 +295,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { if err != nil { t.Fatalf("%+v", err) } - results, err := eventModel.dump(messageStoreName) + results, err := indexedDb.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } @@ -309,7 +306,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { // Now insert a message with a different message ID from the first testMsgId2 := channel.MakeMessageID([]byte(testString), &id.ID{2}) testMsg = buildMessage([]byte(testString), testMsgId2.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, nil, 0, netTime.Now(), + testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, channels.Sent) primaryKey, err := eventModel.receiveHelper(testMsg, false) if err != nil { @@ -327,7 +324,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { // The update to duplicate message ID won't fail, // but it just silently shouldn't happen - results, err = eventModel.dump(messageStoreName) + results, err = indexedDb.Dump(eventModel.db, messageStoreName) if err != nil { t.Fatalf("%+v", err) } diff --git a/indexedDb/init.go b/indexedDb/channels/init.go similarity index 97% rename from indexedDb/init.go rename to indexedDb/channels/init.go index f3f4503dec9284896ce6f66d2db44209640a46ad..0b52434ff260ee3df42c8b0f201527f08377205b 100644 --- a/indexedDb/init.go +++ b/indexedDb/channels/init.go @@ -7,7 +7,7 @@ //go:build js && wasm -package indexedDb +package channels import ( "github.com/hack-pad/go-indexeddb/idb" @@ -15,6 +15,7 @@ import ( 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/storage" "gitlab.com/xx_network/primitives/id" "syscall/js" @@ -58,7 +59,7 @@ func NewWASMEventModel(path string, encryption cryptoChannel.Cipher, func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, cb MessageReceivedCallback) (*wasmModel, error) { // Attempt to open database object - ctx, cancel := newContext() + ctx, cancel := indexedDb.NewContext() defer cancel() openRequest, err := idb.Global().Open(ctx, databaseName, currentVersion, func(db *idb.Database, oldVersion, newVersion uint) error { @@ -109,9 +110,6 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, } // Attempt to ensure the database has been properly initialized - if err != nil { - return nil, err - } openRequest, err = idb.Global().Open(ctx, databaseName, currentVersion, func(db *idb.Database, oldVersion, newVersion uint) error { return nil @@ -209,7 +207,7 @@ func (w *wasmModel) hackTestDb() error { if helper != nil { return helper } - result, err := w.get(messageStoreName, js.ValueOf(msgId)) + result, err := indexedDb.Get(w.db, messageStoreName, js.ValueOf(msgId)) if err != nil { return err } diff --git a/indexedDb/model.go b/indexedDb/channels/model.go similarity index 95% rename from indexedDb/model.go rename to indexedDb/channels/model.go index 839ed29c3c7b6aafe33ab9a0001d7b919964dd18..078d6bd67f7c43d9e50373b7801f69937cfce2ac 100644 --- a/indexedDb/model.go +++ b/indexedDb/channels/model.go @@ -7,7 +7,7 @@ //go:build js && wasm -package indexedDb +package channels import ( "time" @@ -60,8 +60,8 @@ type Message struct { Round uint64 `json:"round"` // User cryptographic Identity struct -- could be pulled out - Pubkey []byte `json:"pubkey"` // Index - DmToken []byte `json:"dm_token"` // Index + Pubkey []byte `json:"pubkey"` + DmToken uint32 `json:"dm_token"` CodesetVersion uint8 `json:"codeset_version"` } diff --git a/indexedDb/dm/implementation.go b/indexedDb/dm/implementation.go new file mode 100644 index 0000000000000000000000000000000000000000..3327959392089e4d30c1a8b09141edc83c136ddc --- /dev/null +++ b/indexedDb/dm/implementation.go @@ -0,0 +1,367 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package channelEventModel + +import ( + "crypto/ed25519" + "encoding/json" + "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/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/xx_network/primitives/id" + "sync" + "syscall/js" + "time" + + "github.com/hack-pad/go-indexeddb/idb" + cryptoChannel "gitlab.com/elixxir/crypto/channel" +) + +// wasmModel implements [dm.Receiver] 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 = indexedDb.Put(w.db, conversationStoreName, convoObj) + if err != nil { + return errors.WithMessagef(parentErr, + "Unable to put Conversation: %+v", err) + } + return nil +} + +// buildMessage is a private helper that converts typical [dm.Receiver] +// 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 []byte, 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 dm.MessageID, 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") + var err error + + // If there is no extant Conversation, create one. + if result, err := indexedDb.Get(w.db, conversationStoreName, + utils.CopyBytesToJS(pubKey)); err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + return 0 + } else if len(result) == 0 { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + jww.ERROR.Printf("%+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) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + 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) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) ReceiveText(messageID dm.MessageID, 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") + var err error + + // If there is no extant Conversation, create one. + if result, err := indexedDb.Get(w.db, conversationStoreName, + utils.CopyBytesToJS(pubKey)); err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + return 0 + } else if len(result) == 0 { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + jww.ERROR.Printf("%+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) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + 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) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) ReceiveReply(messageID dm.MessageID, reactionTo dm.MessageID, + 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") + var err error + + // If there is no extant Conversation, create one. + if result, err := indexedDb.Get(w.db, conversationStoreName, + utils.CopyBytesToJS(pubKey)); err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + return 0 + } else if len(result) == 0 { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + jww.ERROR.Printf("%+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) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + msgToInsert := buildMessage(messageID.Bytes(), reactionTo.Marshal(), 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) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) ReceiveReaction(messageID dm.MessageID, reactionTo dm.MessageID, + 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") + var err error + + // If there is no extant Conversation, create one. + if result, err := indexedDb.Get(w.db, conversationStoreName, + utils.CopyBytesToJS(pubKey)); err != nil { + jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, + "Unable to get Conversation: %+v", err)) + return 0 + } else if len(result) == 0 { + err = w.joinConversation(nickname, pubKey, dmToken, codeset) + jww.ERROR.Printf("%+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) + if err != nil { + jww.ERROR.Printf("Failed to encrypt Message: %+v", err) + return 0 + } + } + + msgToInsert := buildMessage(messageID.Bytes(), nil, textBytes, + pubKey, timestamp, round.ID, dm.ReactionType, status) + + uuid, err := w.receiveHelper(msgToInsert, false) + if err != nil { + jww.ERROR.Printf("Failed to receive Message: %+v", err) + } + + go w.receivedMessageCB(uuid, pubKey, false) + return uuid +} + +func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID dm.MessageID, + 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 := indexedDb.Get(w.db, messageStoreName, key) + if err != nil { + return + } + + // Extract the existing Message and update the Status + newMessage := &Message{} + err = json.Unmarshal([]byte(currentMsg), newMessage) + if err != nil { + return + } + newMessage.Status = uint8(status) + if !messageID.Equals(dm.MessageID{}) { + 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") + } + + // Store message to database + addReq, err := indexedDb.Put(w.db, messageStoreName, messageObj) + if err != nil { + return 0, errors.Errorf("Unable to put Message: %+v", err) + } + res, err := addReq.Result() + if err != nil { + return 0, errors.Errorf("Unable to get Message result: %+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 res.IsUndefined() { + msgID := cryptoChannel.MessageID{} + 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(res.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 cryptoChannel.MessageID) (uint64, + error) { + resultObj, err := indexedDb.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 new file mode 100644 index 0000000000000000000000000000000000000000..d2c3ba1dbb2d6306a39ffc8f5f0b4e6f0159dc73 --- /dev/null +++ b/indexedDb/dm/init.go @@ -0,0 +1,181 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package channelEventModel + +import ( + "crypto/ed25519" + "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" + "syscall/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. +// +// update is true if the row is old and was edited. +type MessageReceivedCallback func(uuid uint64, pubKey ed25519.PublicKey, update bool) + +// 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.Receiver, error) { + databaseName := path + databaseSuffix + return newWASMModel(databaseName, encryption, cb) +} + +// newWASMModel creates the given [idb.Database] and returns a wasmModel. +func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, + cb MessageReceivedCallback) (*wasmModel, error) { + // Attempt to open database object + ctx, cancel := indexedDb.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 + } + + // Wait for database open to finish + db, err := openRequest.Await(ctx) + if err != nil { + return nil, err + } + + // Save the encryption status to storage + encryptionStatus := encryption != nil + loadedEncryptionStatus, err := storage.StoreIndexedDbEncryptionStatus( + databaseName, encryptionStatus) + 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!") + } + + // 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} + + return wrapper, nil +} + +// 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, + } + + // 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 + } + + // Build Channel ObjectStore + conversationStoreOpts := idb.ObjectStoreOptions{ + KeyPath: js.ValueOf(convoPkeyName), + AutoIncrement: false, + } + _, err = db.CreateObjectStore(conversationStoreName, conversationStoreOpts) + if err != nil { + return err + } + + // Get the database name and save it to storage + if databaseName, err := db.Name(); err != nil { + return err + } else if err = storage.StoreIndexedDb(databaseName); err != nil { + return err + } + + return nil +} diff --git a/indexedDb/dm/model.go b/indexedDb/dm/model.go new file mode 100644 index 0000000000000000000000000000000000000000..bb6f34588aa2976c1689b629c6df31219de6b585 --- /dev/null +++ b/indexedDb/dm/model.go @@ -0,0 +1,63 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package channelEventModel + +import ( + "time" +) + +const ( + // Text representation of primary key value (keyPath). + msgPkeyName = "id" + convoPkeyName = "pub_key" + + // Text representation of the names of the various [idb.ObjectStore]. + messageStoreName = "messages" + conversationStoreName = "conversations" + + // Message index names. + messageStoreMessageIndex = "message_id_index" + messageStoreConversationIndex = "conversation_id_index" + messageStoreParentIndex = "parent_message_id_index" + messageStoreTimestampIndex = "timestamp_index" + + // Message keyPath names (must match json struct tags). + messageStoreMessage = "message_id" + messageStoreConversation = "conversation_id" + messageStoreParent = "parent_message_id" + messageStoreTimestamp = "timestamp" +) + +// Message defines the IndexedDb representation of a single Message. +// +// A Message belongs to one Conversation. +// A Message may belong to one Message (Parent). +type Message struct { + ID uint64 `json:"id"` // Matches msgPkeyName + MessageID []byte `json:"message_id"` // Index + ConversationPubKey []byte `json:"conversation_pub_key"` // Index + ParentMessageID []byte `json:"parent_message_id"` // Index + Timestamp time.Time `json:"timestamp"` // Index + Status uint8 `json:"status"` + Text []byte `json:"text"` + Type uint16 `json:"type"` + Round uint64 `json:"round"` +} + +// Conversation defines the IndexedDb representation of a single +// message exchange between two recipients. +// A Conversation has many Message. +type Conversation struct { + Pubkey []byte `json:"pub_key"` // Matches convoPkeyName + Nickname string `json:"nickname"` + Token uint32 `json:"token"` + CodesetVersion uint8 `json:"codeset_version"` + Blocked bool `json:"blocked"` +} diff --git a/indexedDb/utils.go b/indexedDb/utils.go new file mode 100644 index 0000000000000000000000000000000000000000..18904936a8733f69e0c59c7706e22b5152d60412 --- /dev/null +++ b/indexedDb/utils.go @@ -0,0 +1,192 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +// This file contains several generic IndexedDB helper functions that +// may be useful for any IndexedDB implementations. + +package indexedDb + +import ( + "context" + "github.com/hack-pad/go-indexeddb/idb" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/utils" + "syscall/js" + "time" +) + +// dbTimeout is the global timeout for operations with the storage +// [context.Context]. +const dbTimeout = time.Second + +// NewContext builds a context for indexedDb operations. +func NewContext() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), dbTimeout) +} + +// Get is a generic helper for getting values from the given [idb.ObjectStore]. +func Get(db *idb.Database, objectStoreName string, key js.Value) (string, error) { + parentErr := errors.Errorf("failed to Get %s/%s", objectStoreName, key) + + // Prepare the Transaction + txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName) + if err != nil { + return "", errors.WithMessagef(parentErr, + "Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(objectStoreName) + if err != nil { + return "", errors.WithMessagef(parentErr, + "Unable to get ObjectStore: %+v", err) + } + + // Perform the operation + getRequest, err := store.Get(key) + if err != nil { + return "", errors.WithMessagef(parentErr, + "Unable to Get from ObjectStore: %+v", err) + } + + // Wait for the operation to return + ctx, cancel := NewContext() + resultObj, err := getRequest.Await(ctx) + cancel() + if err != nil { + return "", errors.WithMessagef(parentErr, + "Unable to get from ObjectStore: %+v", err) + } + + // Process result into string + resultStr := utils.JsToJson(resultObj) + jww.DEBUG.Printf("Got from %s/%s: %s", objectStoreName, key, resultStr) + return resultStr, nil +} + +// GetIndex is a generic helper for getting values from the given +// [idb.ObjectStore] using the given [idb.Index]. +func GetIndex(db *idb.Database, objectStoreName string, + indexName string, key js.Value) (js.Value, error) { + parentErr := errors.Errorf("failed to GetIndex %s/%s/%s", + objectStoreName, indexName, key) + + // Prepare the Transaction + txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName) + if err != nil { + return js.Value{}, errors.WithMessagef(parentErr, + "Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(objectStoreName) + if err != nil { + return js.Value{}, errors.WithMessagef(parentErr, + "Unable to get ObjectStore: %+v", err) + } + idx, err := store.Index(indexName) + if err != nil { + return js.Value{}, errors.WithMessagef(parentErr, + "Unable to get Index: %+v", err) + } + + // Perform the operation + getRequest, err := idx.Get(key) + if err != nil { + return js.Value{}, errors.WithMessagef(parentErr, + "Unable to Get from ObjectStore: %+v", err) + } + + // Wait for the operation to return + ctx, cancel := NewContext() + resultObj, err := getRequest.Await(ctx) + cancel() + if err != nil { + return js.Value{}, errors.WithMessagef(parentErr, + "Unable to get from ObjectStore: %+v", err) + } + + // Process result into string + resultStr := utils.JsToJson(resultObj) + jww.DEBUG.Printf("Got from %s/%s/%s: %s", + objectStoreName, indexName, key, resultStr) + return resultObj, nil +} + +// Put is a generic helper for putting values into the given [idb.ObjectStore]. +// Equivalent to insert if not exists else update. +func Put(db *idb.Database, objectStoreName string, value js.Value) (*idb.Request, error) { + // Prepare the Transaction + txn, err := db.Transaction(idb.TransactionReadWrite, objectStoreName) + if err != nil { + return nil, errors.Errorf("Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(objectStoreName) + if err != nil { + return nil, errors.Errorf("Unable to get ObjectStore: %+v", err) + } + + // Perform the operation + request, err := store.Put(value) + if err != nil { + return nil, errors.Errorf("Unable to Put: %+v", err) + } + + // Wait for the operation to return + ctx, cancel := NewContext() + err = txn.Await(ctx) + cancel() + if err != nil { + return nil, errors.Errorf("Putting value failed: %+v", err) + } + jww.DEBUG.Printf("Successfully put value in %s: %v", + objectStoreName, value.String()) + return request, nil +} + +// Dump returns the given [idb.ObjectStore] contents to string slice for +// testing and debugging purposes. +func Dump(db *idb.Database, objectStoreName string) ([]string, error) { + parentErr := errors.Errorf("failed to Dump %s", objectStoreName) + + txn, err := db.Transaction(idb.TransactionReadOnly, objectStoreName) + if err != nil { + return nil, errors.WithMessagef(parentErr, + "Unable to create Transaction: %+v", err) + } + store, err := txn.ObjectStore(objectStoreName) + if err != nil { + return nil, errors.WithMessagef(parentErr, + "Unable to get ObjectStore: %+v", err) + } + cursorRequest, err := store.OpenCursor(idb.CursorNext) + if err != nil { + return nil, errors.WithMessagef(parentErr, + "Unable to open Cursor: %+v", err) + } + + // Run the query + jww.DEBUG.Printf("%s values:", objectStoreName) + results := make([]string, 0) + ctx, cancel := NewContext() + err = cursorRequest.Iter(ctx, + func(cursor *idb.CursorWithValue) error { + value, err := cursor.Value() + if err != nil { + return err + } + valueStr := utils.JsToJson(value) + results = append(results, valueStr) + jww.DEBUG.Printf("- %v", valueStr) + return nil + }) + cancel() + if err != nil { + return nil, errors.WithMessagef(parentErr, + "Unable to dump ObjectStore: %+v", err) + } + return results, nil +} diff --git a/wasm/channels.go b/wasm/channels.go index c906ae53f50081b8bdc6c26c55a56992c0ba1bd4..66dd9e656a156c275d692a7a7bfb7ca2efadaf7c 100644 --- a/wasm/channels.go +++ b/wasm/channels.go @@ -11,12 +11,12 @@ package wasm import ( "encoding/base64" + "gitlab.com/elixxir/xxdk-wasm/indexedDb/channels" "gitlab.com/xx_network/primitives/id" "sync" "syscall/js" "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/indexedDb" "gitlab.com/elixxir/xxdk-wasm/utils" ) @@ -394,7 +394,7 @@ func newChannelsManagerWithIndexedDb(cmixID int, privateIdentity []byte, cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update) } - model := indexedDb.NewWASMEventModelBuilder(cipher, messageReceivedCB) + model := channels.NewWASMEventModelBuilder(cipher, messageReceivedCB) promiseFn := func(resolve, reject func(args ...any) js.Value) { cm, err := bindings.NewChannelsManagerGoEventModel( @@ -493,7 +493,7 @@ func loadChannelsManagerWithIndexedDb(cmixID int, storageTag string, cb.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), updated) } - model := indexedDb.NewWASMEventModelBuilder(cipher, messageReceivedCB) + model := channels.NewWASMEventModelBuilder(cipher, messageReceivedCB) promiseFn := func(resolve, reject func(args ...any) js.Value) { cm, err := bindings.LoadChannelsManagerGoEventModel( @@ -1284,7 +1284,7 @@ func (em *eventModel) LeaveChannel(channelID []byte) { // - nickname - The nickname of the sender of the message (string). // - text - The content of the message (string). // - pubKey - The sender's Ed25519 public key (Uint8Array). -// - dmToken - The dmToken (Uint8Array). +// - dmToken - The dmToken (int32). // - codeset - The codeset version (int). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). @@ -1303,7 +1303,7 @@ func (em *eventModel) LeaveChannel(channelID []byte) { // - A non-negative unique UUID for the message that it can be referenced by // later with [eventModel.UpdateSentStatus]. func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname, - text string, pubKey []byte, dmToken []byte, codeset int, timestamp, lease, roundId, msgType, + text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, roundId, msgType, status int64) int64 { uuid := em.receiveMessage(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), nickname, text, @@ -1329,7 +1329,7 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname, // - senderUsername - The username of the sender of the message (string). // - text - The content of the message (string). // - pubKey - The sender's Ed25519 public key (Uint8Array). -// - dmToken - The dmToken (Uint8Array). +// - dmToken - The dmToken (int32). // - codeset - The codeset version (int). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). @@ -1348,7 +1348,7 @@ func (em *eventModel) ReceiveMessage(channelID, messageID []byte, nickname, // - A non-negative unique UUID for the message that it can be referenced by // later with [eventModel.UpdateSentStatus]. func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte, - senderUsername, text string, pubKey []byte, dmToken []byte, codeset int, timestamp, lease, + senderUsername, text string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, roundId, msgType, status int64) int64 { uuid := em.receiveReply(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo), @@ -1374,7 +1374,7 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte, // - senderUsername - The username of the sender of the message (string). // - reaction - The contents of the reaction message (string). // - pubKey - The sender's Ed25519 public key (Uint8Array). -// - dmToken - The dmToken (Uint8Array). +// - dmToken - The dmToken (int32). // - codeset - The codeset version (int). // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). @@ -1393,7 +1393,7 @@ func (em *eventModel) ReceiveReply(channelID, messageID, reactionTo []byte, // - A non-negative unique UUID for the message that it can be referenced by // later with [eventModel.UpdateSentStatus]. func (em *eventModel) ReceiveReaction(channelID, messageID, reactionTo []byte, - senderUsername, reaction string, pubKey []byte, dmToken []byte, codeset int, timestamp, + senderUsername, reaction string, pubKey []byte, dmToken int32, codeset int, timestamp, lease, roundId, msgType, status int64) int64 { uuid := em.receiveReaction(utils.CopyBytesToJS(channelID), utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(reactionTo),