From 1e6c0b38a09dae2a193633e66bda3620d6ab2751 Mon Sep 17 00:00:00 2001
From: Jake Taylor <jake@elixxir.io>
Date: Thu, 15 Dec 2022 13:49:51 -0600
Subject: [PATCH] refactor indexedDb to be more receptive to upcoming project
 changes

---
 indexedDb/{ => channels}/implementation.go    | 211 ++--------------
 .../{ => channels}/implementation_test.go     |  23 +-
 indexedDb/{ => channels}/init.go              |   7 +-
 indexedDb/{ => channels}/model.go             |   4 +-
 indexedDb/utils.go                            | 226 ++++++++++++++++++
 5 files changed, 262 insertions(+), 209 deletions(-)
 rename indexedDb/{ => channels}/implementation.go (68%)
 rename indexedDb/{ => channels}/implementation_test.go (94%)
 rename indexedDb/{ => channels}/init.go (97%)
 rename indexedDb/{ => channels}/model.go (97%)
 create mode 100644 indexedDb/utils.go

diff --git a/indexedDb/implementation.go b/indexedDb/channels/implementation.go
similarity index 68%
rename from indexedDb/implementation.go
rename to indexedDb/channels/implementation.go
index 273a1ec4..46e68fd4 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()
@@ -255,8 +219,8 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID,
 	}
 
 	msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(),
-		replyTo.Bytes(), nickname, textBytes, pubKey, codeset, timestamp, lease,
-		round.ID, mType, status)
+		replyTo.Bytes(), nickname, textBytes, pubKey, codeset,
+		timestamp, lease, round.ID, mType, status)
 
 	uuid, err := w.receiveHelper(msgToInsert, false)
 
@@ -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
 	}
@@ -404,33 +368,14 @@ 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)
-	if err != nil {
-		return 0, errors.Errorf("Unable to upsert Message: %+v", err)
-	}
-
-	// Wait for the operation to return
-	ctx, cancel := newContext()
-	err = txn.Await(ctx)
-	cancel()
+	// Store message to database
+	addReq, err := indexedDb.Put(w.db, messageStoreName, messageObj)
 	if err != nil {
-		return 0, errors.Errorf("Upserting Message failed: %+v", err)
+		return 0, errors.Errorf("Unable to put Message: %+v", err)
 	}
 	res, err := addReq.Result()
 	if err != nil {
-		return 0, errors.Errorf("Getting result from request failed: %+v", err)
+		return 0, errors.Errorf("Unable to get Message result: %+v", err)
 	}
 
 	// NOTE: Sometimes the insert fails to return an error but hits a duplicate
@@ -450,135 +395,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 94%
rename from indexedDb/implementation_test.go
rename to indexedDb/channels/implementation_test.go
index c50be8a4..221494e3 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"
@@ -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)
 	}
@@ -151,8 +152,6 @@ func Test_wasmModel_UUIDTest(t *testing.T) {
 		uuids[i] = uuid
 	}
 
-	_, _ = eventModel.dump(messageStoreName)
-
 	for i := 0; i < 10; i++ {
 		for j := i + 1; j < 10; j++ {
 			if uuids[i] == uuids[j] {
@@ -186,8 +185,6 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) {
 		uuids[i] = uuid
 	}
 
-	_, _ = eventModel.dump(messageStoreName)
-
 	for i := 0; i < 10; i++ {
 		for j := i + 1; j < 10; j++ {
 			if uuids[i] != uuids[j] {
@@ -230,7 +227,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) {
 	}
 
 	// 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)
 	}
@@ -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)
 	}
@@ -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 543ae0ae..0b52434f 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 {
@@ -206,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 97%
rename from indexedDb/model.go
rename to indexedDb/channels/model.go
index c6e29184..02a3ebd4 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,7 +60,7 @@ type Message struct {
 	Round           uint64        `json:"round"`
 
 	// User cryptographic Identity struct -- could be pulled out
-	Pubkey         []byte `json:"pubkey"` // Index
+	Pubkey         []byte `json:"pubkey"`
 	CodesetVersion uint8  `json:"codeset_version"`
 }
 
diff --git a/indexedDb/utils.go b/indexedDb/utils.go
new file mode 100644
index 00000000..7d7e7e6b
--- /dev/null
+++ b/indexedDb/utils.go
@@ -0,0 +1,226 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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
+}
+
+// Delete is a generic helper for removing values from the given [idb.ObjectStore].
+func Delete(db *idb.Database, objectStoreName string, key js.Value) error {
+	parentErr := errors.Errorf("failed to Delete %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
+	deleteRequest, err := store.Delete(key)
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to Get from ObjectStore: %+v", err)
+	}
+
+	// Wait for the operation to return
+	ctx, cancel := NewContext()
+	err = deleteRequest.Await(ctx)
+	cancel()
+	if err != nil {
+		return errors.WithMessagef(parentErr,
+			"Unable to delete from ObjectStore: %+v", err)
+	}
+	return 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
+}
-- 
GitLab