diff --git a/indexedDb/impl/channels/implementation.go b/indexedDb/impl/channels/implementation.go index 5f8273a2ccc9a6b6690c2988b65ee1e1e304deb2..0976d263b00011b7ec4a6a5d6bfce38c2780beb0 100644 --- a/indexedDb/impl/channels/implementation.go +++ b/indexedDb/impl/channels/implementation.go @@ -121,19 +121,19 @@ func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error { "Unable to get Index: %+v", err) } - // Perform the operation + // Set up the operation keyRange, err := idb.NewKeyRangeOnly(impl.EncodeBytes(channelID.Marshal())) cursorRequest, err := index.OpenCursorRange(keyRange, idb.CursorNext) if err != nil { return errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err) } - ctx, cancel := impl.NewContext() - err = cursorRequest.Iter(ctx, + + // Perform the operation + err = impl.SendCursorRequest(cursorRequest, 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) diff --git a/indexedDb/impl/channels/init.go b/indexedDb/impl/channels/init.go index 363440d541029007a6c167d94f8a1fcd66edf4fd..ecc16da447b0db684ca55031a26e6a3d19206320 100644 --- a/indexedDb/impl/channels/init.go +++ b/indexedDb/impl/channels/init.go @@ -81,6 +81,8 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, db, err := openRequest.Await(ctx) if err != nil { return nil, err + } else if ctx.Err() != nil { + return nil, ctx.Err() } wrapper := &wasmModel{ @@ -90,7 +92,6 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, deletedMessageCB: deletedMessageCB, mutedUserCB: mutedUserCB, } - return wrapper, nil } diff --git a/indexedDb/impl/dm/init.go b/indexedDb/impl/dm/init.go index b9bad462303b8000c33713f08170f69ac64f39c2..f99f4ef3e2d7f46f9a5672c8ea998c4afdbe1a77 100644 --- a/indexedDb/impl/dm/init.go +++ b/indexedDb/impl/dm/init.go @@ -75,10 +75,11 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, db, err := openRequest.Await(ctx) if err != nil { return nil, err + } else if ctx.Err() != nil { + return nil, ctx.Err() } wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption} - return wrapper, nil } diff --git a/indexedDb/impl/utils.go b/indexedDb/impl/utils.go index c2756f61d19eda0fc40c266d41c7449213c9d902..b657240b7e7018a031365ffc8514c3fe54141471 100644 --- a/indexedDb/impl/utils.go +++ b/indexedDb/impl/utils.go @@ -30,9 +30,6 @@ const ( // ErrDoesNotExist is an error string for got undefined on Get operations. ErrDoesNotExist = "result is undefined" - - // ErrUniqueConstraint is an error string for failed uniqueness inserts. - ErrUniqueConstraint = "at least one key does not satisfy the uniqueness requirements" ) // NewContext builds a context for indexedDb operations. @@ -45,6 +42,31 @@ func EncodeBytes(input []byte) js.Value { return js.ValueOf(base64.StdEncoding.EncodeToString(input)) } +// SendRequest is a wrapper for the request.Await() method providing a timeout. +func SendRequest(request *idb.Request) (js.Value, error) { + ctx, cancel := NewContext() + defer cancel() + result, err := request.Await(ctx) + if err != nil { + return js.Undefined(), err + } else if ctx.Err() != nil { + return js.Undefined(), ctx.Err() + } + return result, nil +} + +// SendCursorRequest is a wrapper for the cursorRequest.Await() method providing a timeout. +func SendCursorRequest(cur *idb.CursorWithValueRequest, + iterFunc func(cursor *idb.CursorWithValue) error) error { + ctx, cancel := NewContext() + defer cancel() + err := cur.Iter(ctx, iterFunc) + if ctx.Err() != nil { + return ctx.Err() + } + return err +} + // Get is a generic helper for getting values from the given [idb.ObjectStore]. // Only usable by primary key. func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, error) { @@ -62,22 +84,18 @@ func Get(db *idb.Database, objectStoreName string, key js.Value) (js.Value, erro "Unable to get ObjectStore: %+v", err) } - // Perform the operation + // Set up the operation getRequest, err := store.Get(key) if err != nil { return js.Undefined(), 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() + // Perform the operation + resultObj, err := SendRequest(getRequest) if err != nil { return js.Undefined(), errors.WithMessagef(parentErr, "Unable to get from ObjectStore: %+v", err) - } else if err = ctx.Err(); errors.Is(err, context.DeadlineExceeded) { - return js.Null(), errors.Wrapf(err, "timed out after %s", dbTimeout) } else if resultObj.IsUndefined() { return js.Undefined(), errors.WithMessagef(parentErr, "Unable to get from ObjectStore: %s", ErrDoesNotExist) @@ -105,14 +123,15 @@ func GetAll(db *idb.Database, objectStoreName string) ([]js.Value, error) { "Unable to get ObjectStore: %+v", err) } - // Perform the operation - result := make([]js.Value, 0) + // Set up the operation cursorRequest, err := store.OpenCursor(idb.CursorNext) if err != nil { return nil, errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err) } - ctx, cancel := NewContext() - err = cursorRequest.Iter(ctx, + result := make([]js.Value, 0) + + // Perform the operation + err = SendCursorRequest(cursorRequest, func(cursor *idb.CursorWithValue) error { row, err := cursor.Value() if err != nil { @@ -121,7 +140,6 @@ func GetAll(db *idb.Database, objectStoreName string) ([]js.Value, error) { result = append(result, row) return nil }) - cancel() if err != nil { return nil, errors.WithMessagef(parentErr, err.Error()) } @@ -152,22 +170,18 @@ func GetIndex(db *idb.Database, objectStoreName, "Unable to get Index: %+v", err) } - // Perform the operation + // Set up the operation getRequest, err := idx.Get(key) if err != nil { return js.Undefined(), 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() + // Perform the operation + resultObj, err := SendRequest(getRequest) if err != nil { return js.Undefined(), errors.WithMessagef(parentErr, "Unable to get from ObjectStore: %+v", err) - } else if err = ctx.Err(); errors.Is(err, context.DeadlineExceeded) { - return js.Null(), errors.Wrapf(err, "timed out after %s", dbTimeout) } else if resultObj.IsUndefined() { return js.Undefined(), errors.WithMessagef(parentErr, "Unable to get from ObjectStore: %s", ErrDoesNotExist) @@ -193,25 +207,21 @@ func Put(db *idb.Database, objectStoreName string, value js.Value) (js.Value, er return js.Undefined(), errors.Errorf("Unable to get ObjectStore: %+v", err) } - // Perform the operation + // Set up the operation request, err := store.Put(value) if err != nil { return js.Undefined(), errors.Errorf("Unable to Put: %+v", err) } - // Wait for the operation to return - ctx, cancel := NewContext() - result, err := request.Await(ctx) - cancel() + // Perform the operation + resultObj, err := SendRequest(request) if err != nil { return js.Undefined(), errors.Errorf("Putting value failed: %+v\n%s", err, utils.JsToJson(value)) - } else if err = ctx.Err(); errors.Is(err, context.DeadlineExceeded) { - return js.Null(), errors.Wrapf(err, "timed out after %s", dbTimeout) } jww.DEBUG.Printf("Successfully put value in %s: %s", objectStoreName, utils.JsToJson(value)) - return result, nil + return resultObj, nil } // Delete is a generic helper for removing values from the given @@ -232,21 +242,17 @@ func Delete(db *idb.Database, objectStoreName string, key js.Value) error { } // Perform the operation - _, err = store.Delete(key) + deleteRequest, err := store.Delete(key) if err != nil { return errors.WithMessagef(parentErr, "Unable to Delete from ObjectStore: %+v", err) } - // Wait for the operation to return - ctx, cancel := NewContext() - err = txn.Await(ctx) - cancel() + // Perform the operation + _, err = SendRequest(deleteRequest.Request) if err != nil { return errors.WithMessagef(parentErr, "Unable to Delete from ObjectStore: %+v", err) - } else if err = ctx.Err(); errors.Is(err, context.DeadlineExceeded) { - return errors.Wrapf(err, "timed out after %s", dbTimeout) } jww.DEBUG.Printf("Successfully deleted value at %s/%s", objectStoreName, utils.JsToJson(key)) @@ -290,17 +296,18 @@ func Dump(db *idb.Database, objectStoreName string) ([]string, error) { return nil, errors.WithMessagef(parentErr, "Unable to get ObjectStore: %+v", err) } + + // Set up the operation 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, + + // Perform the operation + err = SendCursorRequest(cursorRequest, func(cursor *idb.CursorWithValue) error { value, err := cursor.Value() if err != nil { @@ -311,7 +318,6 @@ func Dump(db *idb.Database, objectStoreName string) ([]string, error) { jww.DEBUG.Printf("- %v", valueStr) return nil }) - cancel() if err != nil { return nil, errors.WithMessagef(parentErr, "Unable to dump ObjectStore: %+v", err) diff --git a/indexedDb/impl/utils_test.go b/indexedDb/impl/utils_test.go index 00e235834c44788905af73cafd7448961708a4dc..ba6700356fb5e434ab5e8850373c9bb6a2775116 100644 --- a/indexedDb/impl/utils_test.go +++ b/indexedDb/impl/utils_test.go @@ -11,9 +11,11 @@ package impl import ( "github.com/hack-pad/go-indexeddb/idb" + jww "github.com/spf13/jwalterweatherman" "strings" "syscall/js" "testing" + "time" ) // Error path: Tests that Get returns an error when trying to get a message that @@ -92,3 +94,52 @@ func newTestDB(name, index string, t *testing.T) *idb.Database { return db } + +// TestBenchmark ensures IndexedDb can take at least n operations per second. +func TestBenchmark(t *testing.T) { + jww.SetStdoutThreshold(jww.LevelInfo) + benchmarkDb(50, t) +} + +// benchmarkDb sends n operations to IndexedDb and prints errors. +func benchmarkDb(n int, t *testing.T) { + jww.INFO.Printf("Benchmarking IndexedDb: %d total.", n) + + objectStoreName := "test" + testValue := js.ValueOf(make(map[string]interface{})) + db := newTestDB(objectStoreName, "index", t) + + type metric struct { + didSucceed bool + duration time.Duration + } + done := make(chan metric) + + // Spawn n operations at the same time + startTime := time.Now() + for i := 0; i < n; i++ { + go func() { + opStart := time.Now() + _, err := Put(db, objectStoreName, testValue) + done <- metric{ + didSucceed: err == nil, + duration: time.Since(opStart), + } + }() + } + + // Wait for all to complete + didSucceed := true + for i := 0; i < n; i++ { + result := <-done + if !result.didSucceed { + didSucceed = false + } + jww.DEBUG.Printf("Operation time: %s", result.duration) + } + + timeElapsed := time.Since(startTime) + jww.INFO.Printf("Benchmarking complete. Succeeded: %t\n"+ + "Took %s, Average of %s.", + didSucceed, timeElapsed, timeElapsed/time.Duration(n)) +}