diff --git a/main.go b/main.go index 8ee45ddf9f458caa6f888d05887d4f5268ea1e64..41508da84b647d752c619765ed30f00f23cc6ee5 100644 --- a/main.go +++ b/main.go @@ -11,10 +11,11 @@ package main import ( "fmt" - "github.com/spf13/cobra" "os" "syscall/js" + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/xxdk-wasm/logging" @@ -147,6 +148,8 @@ func setGlobals() { // wasm/cmix.go js.Global().Set("NewCmix", js.FuncOf(wasm.NewCmix)) js.Global().Set("LoadCmix", js.FuncOf(wasm.LoadCmix)) + js.Global().Set("LoadSynchronizedCmix", + js.FuncOf(wasm.LoadSyncrhonizedCmix)) // wasm/delivery.go js.Global().Set("SetDashboardURL", js.FuncOf(wasm.SetDashboardURL)) @@ -231,10 +234,6 @@ func setGlobals() { js.Global().Set("Listen", js.FuncOf(wasm.Listen)) // wasm/sync.go - js.Global().Set("NewFileSystemRemoteStorage", - js.FuncOf(wasm.NewFileSystemRemoteStorage)) - js.Global().Set("NewOrLoadSyncRemoteKV", - js.FuncOf(wasm.NewOrLoadSyncRemoteKV)) // wasm/timeNow.go js.Global().Set("SetTimeSource", js.FuncOf(wasm.SetTimeSource)) @@ -258,7 +257,7 @@ func setGlobals() { var ( logLevel, fileLogLevel jww.Threshold - maxLogFileSizeMB int + maxLogFileSizeMB int workerScriptURL, workerName string ) diff --git a/wasm/cmix.go b/wasm/cmix.go index e3430f4cf4b489ad7480ab53522fac2631c7cbce..83a76767c84878f1fd8c6df9e4faccb657510caa 100644 --- a/wasm/cmix.go +++ b/wasm/cmix.go @@ -10,9 +10,10 @@ package wasm import ( + "syscall/js" + "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/xxdk-wasm/utils" - "syscall/js" ) // Cmix wraps the [bindings.Cmix] object so its methods can be wrapped to be @@ -29,6 +30,7 @@ func newCmixJS(api *bindings.Cmix) map[string]any { // cmix.go "GetID": js.FuncOf(c.GetID), "GetReceptionID": js.FuncOf(c.GetReceptionID), + "GetRemoteKV": js.FuncOf(c.GetRemoteKV), "EKVGet": js.FuncOf(c.EKVGet), "EKVSet": js.FuncOf(c.EKVSet), @@ -142,6 +144,37 @@ func LoadCmix(_ js.Value, args []js.Value) any { return utils.CreatePromise(promiseFn) } +// LoadSyncrhonizedCmix will [LoadCmix] using a RemoteStore to establish +// a synchronized RemoteKV. +// +// Parameters: +// - args[0] - Storage directory path (string). +// - args[1] - Password used for storage (Uint8Array). +// - args[2] - Javascript [RemoteStore] implementation. +// - args[3] - JSON of [xxdk.CMIXParams] (Uint8Array). +// +// Returns a promise: +// - Resolves to a Javascript representation of the [Cmix] object. +// - Rejected with an error if loading [Cmix] fails. +func LoadSyncrhonizedCmix(_ js.Value, args []js.Value) any { + storageDir := args[0].String() + password := utils.CopyBytesToGo(args[1]) + rs := newRemoteStore(args[2]) + cmixParamsJSON := utils.CopyBytesToGo(args[3]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + net, err := bindings.LoadSynchronizedCmix(storageDir, password, + rs, cmixParamsJSON) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(newCmixJS(net)) + } + } + + return utils.CreatePromise(promiseFn) +} + // GetID returns the ID for this [bindings.Cmix] in the cmixTracker. // // Returns: @@ -158,6 +191,20 @@ func (c *Cmix) GetReceptionID(js.Value, []js.Value) any { return utils.CopyBytesToJS(c.api.GetReceptionID()) } +// GetRemoteKV returns the cMix RemoteKV +// +// Returns a promise: +// - Resolves with the RemoteKV object. +func (c *Cmix) GetRemoteKV(_ js.Value, args []js.Value) any { + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + kv := c.api.GetRemoteKV() + resolve(newRemoteKvJS(kv)) + } + + return utils.CreatePromise(promiseFn) +} + // EKVGet allows access to a value inside the secure encrypted key value store. // // Parameters: diff --git a/wasm/collective.go b/wasm/collective.go new file mode 100644 index 0000000000000000000000000000000000000000..3b1f62fd105045627c23b3f9a8cf593d34be5949 --- /dev/null +++ b/wasm/collective.go @@ -0,0 +1,598 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 wasm + +import ( + "syscall/js" + + "gitlab.com/elixxir/client/v4/bindings" + "gitlab.com/elixxir/xxdk-wasm/utils" +) + +//////////////////////////////////////////////////////////////////////////////// +// RemoteKV Methods // +//////////////////////////////////////////////////////////////////////////////// + +// RemoteKV wraps the [bindings.RemoteKV] object so its methods can be wrapped +// to be Javascript compatible. +type RemoteKV struct { + api *bindings.RemoteKV +} + +// newRemoteKvJS creates a new Javascript compatible object (map[string]any) +// that matches the [RemoteKV] structure. +func newRemoteKvJS(api *bindings.RemoteKV) map[string]any { + rkv := RemoteKV{api} + rkvMap := map[string]any{ + "Get": js.FuncOf(rkv.Get), + "Delete": js.FuncOf(rkv.Delete), + "Set": js.FuncOf(rkv.Set), + "GetPrefix": js.FuncOf(rkv.GetPrefix), + "HasPrefx": js.FuncOf(rkv.HasPrefix), + "Prefix": js.FuncOf(rkv.Prefix), + "Root": js.FuncOf(rkv.Root), + "IsMemStore": js.FuncOf(rkv.IsMemStore), + "GetFullKey": js.FuncOf(rkv.GetFullKey), + "Transaction": js.FuncOf(rkv.Transaction), + "StoreMapElement": js.FuncOf(rkv.StoreMapElement), + "StoreMap": js.FuncOf(rkv.StoreMap), + "DeleteMapElement": js.FuncOf(rkv.DeleteMapElement), + "GetMap": js.FuncOf(rkv.GetMap), + "GetMapElement": js.FuncOf(rkv.GetMapElement), + "ListenOnRemoteKey": js.FuncOf(rkv.ListenOnRemoteKey), + "ListenOnRemoteMap": js.FuncOf(rkv.ListenOnRemoteMap), + } + + return rkvMap +} + +// Get returns the object stored at the specified version. +// returns a json of [versioned.Object]. +// +// Parameters: +// - args[0] - key to access, a string +// - args[1] - version, an integer +// +// Returns a promise: +// - Resolves to JSON of a [versioned.Object], e.g.: +// {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z","Data":"bm90IHVwZ3JhZGVk"} +// - Rejected with an access error. Note: File does not exist errors +// are returned whent key is not set. +func (r *RemoteKV) Get(_ js.Value, args []js.Value) any { + key := args[0].String() + version := int64(args[1].Int()) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + value, err := r.api.Get(key, version) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(value)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// Delete removes a given key from the data store. +// +// Parameters: +// - args[0] - key to access, a string +// - args[1] - version, an integer +// +// Returns a promise: +// - Rejected with an access error. Note: File does not exist errors +// are returned whent key is not set. +func (r *RemoteKV) Delete(_ js.Value, args []js.Value) any { + key := args[0].String() + version := int64(args[1].Int()) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := r.api.Delete(key, version) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + +// Set upserts new data into the storage +// When calling this, you are responsible for prefixing the +// key with the correct type optionally unique id! Call +// GetFullKey() to do so. +// The [Object] should contain the versioning if you are +// maintaining such a functionality. +// +// Parameters: +// - args[0] - the key string +// - args[1] - the [versioned.Object] JSON value, e.g.: +// {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z", +// "Data":"bm90IHVwZ3JhZGVk"} +// +// Returns a promise: +// - Rejected with an access error. +func (r *RemoteKV) Set(_ js.Value, args []js.Value) any { + key := args[0].String() + value := utils.CopyBytesToGo(args[1]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := r.api.Set(key, value) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + +// GetPrefix returns the full Prefix of the KV +// Returns a string via a Promise +func (r *RemoteKV) GetPrefix(_ js.Value, args []js.Value) any { + promiseFn := func(resolve, reject func(args ...any) js.Value) { + prefix := r.api.GetPrefix() + resolve(prefix) + } + + return utils.CreatePromise(promiseFn) +} + +// HasPrefix returns whether this prefix exists in the KV +// +// Parameters: +// - args[0] - the prefix string to check for. +// +// Returns a bool via a promise. +func (r *RemoteKV) HasPrefix(_ js.Value, args []js.Value) any { + prefix := args[0].String() + promiseFn := func(resolve, reject func(args ...any) js.Value) { + resolve(r.api.HasPrefix(prefix)) + } + + return utils.CreatePromise(promiseFn) +} + +// Prefix returns a new KV with the new prefix appending +// +// Parameters: +// - args[0] - the prefix to append to the list of prefixes +// +// Returns a promise: +// - Resolves to a new RemoteKV +// - Rejected with an error. +func (r *RemoteKV) Prefix(_ js.Value, args []js.Value) any { + prefix := args[0].String() + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + newAPI, err := r.api.Prefix(prefix) + + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(newRemoteKvJS(newAPI)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// Root returns the KV with no prefixes +func (r *RemoteKV) Root(_ js.Value, args []js.Value) any { + promiseFn := func(resolve, reject func(args ...any) js.Value) { + newAPI, err := r.api.Root() + + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(newRemoteKvJS(newAPI)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// IsMemStore returns true if the underlying KV is memory based +func (r *RemoteKV) IsMemStore(_ js.Value, args []js.Value) any { + promiseFn := func(resolve, reject func(args ...any) js.Value) { + resolve(r.api.IsMemStore()) + } + + return utils.CreatePromise(promiseFn) +} + +// GetFullKey returns the key with all prefixes appended +func (r *RemoteKV) GetFullKey(_ js.Value, args []js.Value) any { + key := args[0].String() + version := int64(args[1].Int()) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + fullKey := r.api.GetFullKey(key, version) + resolve(fullKey) + } + + return utils.CreatePromise(promiseFn) +} + +// Transaction locks a key while it is being mutated then stores the result +// and returns the old value and if it existed in a JSON object. +// Transactions cannot be remote operations +// If the op returns an error, the operation will be aborted. +func (r *RemoteKV) Transaction(_ js.Value, args []js.Value) any { + promiseFn := func(resolve, reject func(args ...any) js.Value) { + reject("unimplemented") + } + + return utils.CreatePromise(promiseFn) +} + +// StoreMapElement stores a versioned map element into the KV. This relies +// on the underlying remote [KV.StoreMapElement] function to lock and control +// updates, but it uses [versioned.Object] values. +// All Map storage functions update the remote. +// valueJSON is a json of a versioned.Object +// +// Parameters: +// - args[0] - the mapName string +// - args[1] - the elementKey string +// - args[2] - the [versioned.Object] JSON value, e.g.: +// {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z", +// "Data":"bm90IHVwZ3JhZGVk"} +// - args[3] - the version int +// +// Returns a promise with an error if any +func (r *RemoteKV) StoreMapElement(_ js.Value, args []js.Value) any { + mapName := args[0].String() + elementKey := args[1].String() + val := utils.CopyBytesToGo(args[2]) + version := int64(args[3].Int()) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := r.api.StoreMapElement(mapName, elementKey, val, version) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + +// StoreMap saves a versioned map element into the KV. This relies +// on the underlying remote [KV.StoreMap] function to lock and control +// updates, but it uses [versioned.Object] values. +// All Map storage functions update the remote. +// valueJSON is a json of map[string]*versioned.Object +// +// Parameters: +// - args[0] - the mapName string +// - args[1] - the [map[string]versioned.Object] JSON value, e.g.: +// {"elementKey": {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z", +// "Data":"bm90IHVwZ3JhZGVk"}} +// - args[2] - the version int +// +// Returns a promise with an error if any +func (r *RemoteKV) StoreMap(_ js.Value, args []js.Value) any { + mapName := args[0].String() + val := utils.CopyBytesToGo(args[1]) + version := int64(args[2].Int()) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := r.api.StoreMap(mapName, val, version) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + +// DeleteMapElement removes a versioned map element from the KV. +// +// Parameters: +// - args[0] - the mapName string +// - args[1] - the elementKey string +// - args[2] - the version int +// +// Returns a promise with an error if any or the json of the deleted +// [versioned.Object], e.g.: +// +// {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z", +// "Data":"bm90IHVwZ3JhZGVk"} +func (r *RemoteKV) DeleteMapElement(_ js.Value, args []js.Value) any { + mapName := args[0].String() + elementKey := args[1].String() + version := int64(args[2].Int()) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + deleted, err := r.api.DeleteMapElement(mapName, elementKey, + version) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(deleted)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// GetMap loads a versioned map from the KV. This relies +// on the underlying remote [KV.GetMap] function to lock and control +// updates, but it uses [versioned.Object] values. +// +// Parameters: +// - args[0] - the mapName string +// - args[1] - the version int +// +// Returns a promise with an error if any or the +// the [map[string]versioned.Object] JSON value, e.g.: +// +// {"elementKey": {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z", +// "Data":"bm90IHVwZ3JhZGVk"}} +func (r *RemoteKV) GetMap(_ js.Value, args []js.Value) any { + mapName := args[0].String() + version := int64(args[1].Int()) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + mapJSON, err := r.api.GetMap(mapName, version) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(mapJSON)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// GetMapElement loads a versioned map element from the KV. This relies +// on the underlying remote [KV.GetMapElement] function to lock and control +// updates, but it uses [versioned.Object] values. +// Parameters: +// - args[0] - the mapName string +// - args[1] - the elementKey string +// - args[2] - the version int +// +// Returns a promise with an error if any or the json of the +// [versioned.Object], e.g.: +// +// {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z", +// "Data":"bm90IHVwZ3JhZGVk"} +func (r *RemoteKV) GetMapElement(_ js.Value, args []js.Value) any { + mapName := args[0].String() + elementKey := args[1].String() + version := int64(args[2].Int()) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + deleted, err := r.api.GetMapElement(mapName, elementKey, + version) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(deleted)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// ListenOnRemoteKey sets up a callback listener for the object specified +// by the key and version. It returns the current [versioned.Object] JSON +// of the value. +// Parameters: +// - args[0] - the key string +// - args[1] - the version int +// - args[2] - the [KeyChangedByRemoteCallback] javascript callback +// +// Returns a promise with an error if any or the json of the existing +// [versioned.Object], e.g.: +// +// {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z", +// "Data":"bm90IHVwZ3JhZGVk"} +func (r *RemoteKV) ListenOnRemoteKey(_ js.Value, args []js.Value) any { + key := args[0].String() + version := int64(args[1].Int()) + cb := newKeyChangedByRemoteCallback(args[2]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + deleted, err := r.api.ListenOnRemoteKey(key, version, cb) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(deleted)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// ListenOnRemoteMap allows the caller to receive updates when +// the map or map elements are updated. Returns a JSON of +// map[string]versioned.Object of the current map value. +// Parameters: +// - args[0] - the mapName string +// - args[1] - the version int +// - args[2] - the [MapChangedByRemoteCallback] javascript callback +// +// Returns a promise with an error if any or the json of the existing +// the [map[string]versioned.Object] JSON value, e.g.: +// +// {"elementKey": {"Version":1,"Timestamp":"2023-05-13T00:50:03.889192694Z", +// "Data":"bm90IHVwZ3JhZGVk"}} +func (r *RemoteKV) ListenOnRemoteMap(_ js.Value, args []js.Value) any { + mapName := args[0].String() + version := int64(args[1].Int()) + cb := newMapChangedByRemoteCallback(args[2]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + deleted, err := r.api.ListenOnRemoteMap(mapName, version, cb) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve(utils.CopyBytesToJS(deleted)) + } + } + + return utils.CreatePromise(promiseFn) +} + +//////////////////////////////////////////////////////////////////////////////// +// RemoteStore // +//////////////////////////////////////////////////////////////////////////////// + +// RemoteStore wraps Javascript callbacks to adhere to the +// [bindings.RemoteStore] interface. +type RemoteStore struct { + read func(args ...any) js.Value + write func(args ...any) js.Value + getLastModified func(args ...any) js.Value + getLastWrite func(args ...any) js.Value + readDir func(args ...any) js.Value +} + +// newRemoteStoreCallbacks maps the functions of the Javascript object matching +// [bindings.RemoteStore] to a RemoteStoreCallbacks. +func newRemoteStore(arg js.Value) *RemoteStore { + return &RemoteStore{ + read: utils.WrapCB(arg, "Read"), + write: utils.WrapCB(arg, "Write"), + getLastModified: utils.WrapCB(arg, "GetLastModified"), + getLastWrite: utils.WrapCB(arg, "GetLastWrite"), + readDir: utils.WrapCB(arg, "ReadDir"), + } +} + +// Read impelements [bindings.RemoteStore.Read] +// +// Parameters: +// - path - The file path to read from (string). +// +// Returns: +// - The file data (Uint8Array). +// - Catches any thrown errors (of type Error) and returns it as an error. +func (rsCB *RemoteStore) Read(path string) ([]byte, error) { + + fn := func() js.Value { return rsCB.read(path) } + v, err := utils.RunAndCatch(fn) + if err != nil { + return nil, err + } + return utils.CopyBytesToGo(v), err +} + +// Write implements [bindings.RemoteStore.Write] +// +// Parameters: +// - path - The file path to write to (string). +// - data - The file data to write (Uint8Array). +// +// Returns: +// - Catches any thrown errors (of type Error) and returns it as an error. +func (rsCB *RemoteStore) Write(path string, data []byte) error { + fn := func() js.Value { return rsCB.write(path, utils.CopyBytesToJS(data)) } + _, err := utils.RunAndCatch(fn) + return err +} + +// GetLastModified implements [bindings.RemoteStore.GetLastModified] +// +// Parameters: +// - path - The file path (string). +// +// Returns: +// - JSON of [bindings.RemoteStoreReport] (Uint8Array). +// - Catches any thrown errors (of type Error) and returns it as an error. +func (rsCB *RemoteStore) GetLastModified(path string) ([]byte, error) { + fn := func() js.Value { return rsCB.getLastModified(path) } + v, err := utils.RunAndCatch(fn) + if err != nil { + return nil, err + } + return utils.CopyBytesToGo(v), err +} + +// GetLastWrite implements [bindings.RemoteStore.GetLastWrite() +// +// Returns: +// - JSON of [bindings.RemoteStoreReport] (Uint8Array). +// - Catches any thrown errors (of type Error) and returns it as an error. +func (rsCB *RemoteStore) GetLastWrite() ([]byte, error) { + fn := func() js.Value { return rsCB.getLastWrite() } + v, err := utils.RunAndCatch(fn) + if err != nil { + return nil, err + } + return utils.CopyBytesToGo(v), err +} + +// ReadDir implements [bindings.RemoteStore.ReadDir] +// +// Parameters: +// - path - The file path (string). +// +// Returns: +// - JSON of []string (Uint8Array). +// - Catches any thrown errors (of type Error) and returns it as an error. +func (rsCB *RemoteStore) ReadDir(path string) ([]byte, error) { + fn := func() js.Value { return rsCB.readDir(path) } + v, err := utils.RunAndCatch(fn) + if err != nil { + return nil, err + } + return utils.CopyBytesToGo(v), err +} + +//////////////////////////////////////////////////////////////////////////////// +// Callbacks // +//////////////////////////////////////////////////////////////////////////////// + +// KeyChangedByRemoteCallback wraps the passed javascript function and +// implements [bindings.KeyChangedByRemoteCallback] +type KeyChangedByRemoteCallback struct { + callback func(args ...any) js.Value +} + +func (k *KeyChangedByRemoteCallback) Callback(key string, old, new []byte, + opType int8) { + k.callback(key, utils.CopyBytesToJS(old), utils.CopyBytesToJS(new), + opType) +} + +func newKeyChangedByRemoteCallback( + jsFunc js.Value) *KeyChangedByRemoteCallback { + return &KeyChangedByRemoteCallback{ + callback: utils.WrapCB(jsFunc, "Callback"), + } +} + +// MapChangedByRemoteCallback wraps the passed javascript function and +// implements [bindings.KeyChangedByRemoteCallback] +type MapChangedByRemoteCallback struct { + callback func(args ...any) js.Value +} + +func (m *MapChangedByRemoteCallback) Callback(mapName string, + editsJSON []byte) { + m.callback(mapName, utils.CopyBytesToJS(editsJSON)) +} + +func newMapChangedByRemoteCallback( + jsFunc js.Value) *MapChangedByRemoteCallback { + return &MapChangedByRemoteCallback{ + callback: utils.WrapCB(jsFunc, "Callback"), + } +} diff --git a/wasm/sync_test.go b/wasm/collective_test.go similarity index 56% rename from wasm/sync_test.go rename to wasm/collective_test.go index 4ab3b8abb818f63f6085a41905354f8b5617be73..8c512adce32127a2a448882167799e6268454fea 100644 --- a/wasm/sync_test.go +++ b/wasm/collective_test.go @@ -16,48 +16,6 @@ import ( "gitlab.com/elixxir/client/v4/bindings" ) -// Tests that the map representing RemoteStoreFileSystem returned by -// newRemoteStoreFileSystemJS contains all of the methods on -// RemoteStoreFileSystem. -func Test_newRemoteStoreFileSystemJS(t *testing.T) { - rsfType := reflect.TypeOf(&RemoteStoreFileSystem{}) - - rsf := newRemoteStoreFileSystemJS(&bindings.RemoteStoreFileSystem{}) - if len(rsf) != rsfType.NumMethod() { - t.Errorf("RemoteStoreFileSystem JS object does not have all methods."+ - "\nexpected: %d\nreceived: %d", rsfType.NumMethod(), len(rsf)) - } - - for i := 0; i < rsfType.NumMethod(); i++ { - method := rsfType.Method(i) - - if _, exists := rsf[method.Name]; !exists { - t.Errorf("Method %s does not exist.", method.Name) - } - } -} - -// Tests that RemoteStoreFileSystem has all the methods that -// [bindings.RemoteStoreFileSystem] has. -func Test_RemoteStoreFileSystemMethods(t *testing.T) { - rsfType := reflect.TypeOf(&RemoteStoreFileSystem{}) - binRsfType := reflect.TypeOf(&bindings.RemoteStoreFileSystem{}) - - if binRsfType.NumMethod() != rsfType.NumMethod() { - t.Errorf("WASM RemoteStoreFileSystem object does not have all methods "+ - "from bindings.\nexpected: %d\nreceived: %d", - binRsfType.NumMethod(), rsfType.NumMethod()) - } - - for i := 0; i < binRsfType.NumMethod(); i++ { - method := binRsfType.Method(i) - - if _, exists := rsfType.MethodByName(method.Name); !exists { - t.Errorf("Method %s does not exist.", method.Name) - } - } -} - // Tests that the map representing RemoteKV returned by newRemoteKvJS contains // all of the methods on RemoteKV. func Test_newRemoteKvJS(t *testing.T) { diff --git a/wasm/sync.go b/wasm/sync.go deleted file mode 100644 index c01d28e93c0404c027c4f7155ed8411aff9dcc39..0000000000000000000000000000000000000000 --- a/wasm/sync.go +++ /dev/null @@ -1,423 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -//go:build js && wasm - -package wasm - -import ( - "syscall/js" - - "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" -) - -// TODO: add tests - -//////////////////////////////////////////////////////////////////////////////// -// Remote Storage Interface and Implementation(s) // -//////////////////////////////////////////////////////////////////////////////// - -// RemoteStoreFileSystem wraps the [bindings.RemoteStoreFileSystem] object so -// its methods can be wrapped to be Javascript compatible. -type RemoteStoreFileSystem struct { - api *bindings.RemoteStoreFileSystem -} - -// newRemoteStoreFileSystemJS creates a new Javascript compatible object -// (map[string]any) that matches the [RemoteStoreFileSystem] structure. -func newRemoteStoreFileSystemJS(api *bindings.RemoteStoreFileSystem) map[string]any { - rsf := RemoteStoreFileSystem{api} - rsfMap := map[string]any{ - "Read": js.FuncOf(rsf.Read), - "Write": js.FuncOf(rsf.Write), - "GetLastModified": js.FuncOf(rsf.GetLastModified), - "GetLastWrite": js.FuncOf(rsf.GetLastWrite), - } - - return rsfMap -} - -// NewFileSystemRemoteStorage is a constructor for [RemoteStoreFileSystem]. -// -// Parameters: -// - args[0] - The base directory that all file operations will be performed. -// It must contain a file delimiter (i.e., `/`) (string). -// -// Returns: -// - A Javascript representation of the [RemoteStoreFileSystem] object. -func NewFileSystemRemoteStorage(_ js.Value, args []js.Value) any { - baseDir := args[0].String() - api := bindings.NewFileSystemRemoteStorage(baseDir) - - return newRemoteStoreFileSystemJS(api) -} - -// Read reads from the provided file path and returns the data at that path. -// An error is returned if it failed to read the file. -// -// Parameters: -// - args[0] - The file path to read from (string). -// -// Returns a promise: -// - Resolves to the file data (Uint8Array) -// - Rejected with an error if reading from the file fails. -func (rsf *RemoteStoreFileSystem) Read(_ js.Value, args []js.Value) any { - path := args[0].String() - - promiseFn := func(resolve, reject func(args ...any) js.Value) { - data, err := rsf.api.Read(path) - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve(utils.CopyBytesToJS(data)) - } - } - - return utils.CreatePromise(promiseFn) -} - -// Write writes to the file path the provided data. An error is returned if it -// fails to write to file. -// -// Parameters: -// - args[0] - The file path to write to (string). -// - args[1] - The file data to write (Uint8Array). -// -// Returns a promise: -// - Resolves on success (void). -// - Rejected with an error if writing to the file fails. -func (rsf *RemoteStoreFileSystem) Write(_ js.Value, args []js.Value) any { - path := args[0].String() - data := utils.CopyBytesToGo(args[1]) - - promiseFn := func(resolve, reject func(args ...any) js.Value) { - err := rsf.api.Write(path, data) - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve() - } - } - - return utils.CreatePromise(promiseFn) -} - -// GetLastModified returns when the file at the given file path was last -// modified. If the implementation that adheres to this interface does not -// support this, [Write] or [Read] should be implemented to either write a -// separate timestamp file or add a prefix. -// -// Parameters: -// - args[0] - The file path (string). -// -// Returns a promise: -// - Resolves to the JSON of [bindings.RemoteStoreReport] (Uint8Array). -// - Rejected with an error on failure. -func (rsf *RemoteStoreFileSystem) GetLastModified(_ js.Value, args []js.Value) any { - path := args[0].String() - - promiseFn := func(resolve, reject func(args ...any) js.Value) { - report, err := rsf.api.GetLastModified(path) - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve(utils.CopyBytesToJS(report)) - } - } - - return utils.CreatePromise(promiseFn) -} - -// GetLastWrite retrieves the most recent successful write operation that was -// received by [RemoteStoreFileSystem]. -// -// Returns a promise: -// - Resolves to the JSON of [bindings.RemoteStoreReport] (Uint8Array). -// - Rejected with an error on failure. -func (rsf *RemoteStoreFileSystem) GetLastWrite(js.Value, []js.Value) any { - promiseFn := func(resolve, reject func(args ...any) js.Value) { - report, err := rsf.api.GetLastWrite() - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve(utils.CopyBytesToJS(report)) - } - } - - return utils.CreatePromise(promiseFn) -} - -//////////////////////////////////////////////////////////////////////////////// -// RemoteKV Methods // -//////////////////////////////////////////////////////////////////////////////// - -// RemoteKV wraps the [bindings.RemoteKV] object so its methods can be wrapped -// to be Javascript compatible. -type RemoteKV struct { - api *bindings.RemoteKV -} - -// newRemoteKvJS creates a new Javascript compatible object (map[string]any) -// that matches the [RemoteKV] structure. -func newRemoteKvJS(api *bindings.RemoteKV) map[string]any { - rkv := RemoteKV{api} - rkvMap := map[string]any{ - "Write": js.FuncOf(rkv.Write), - "Read": js.FuncOf(rkv.Read), - "GetList": js.FuncOf(rkv.GetList), - } - - return rkvMap -} - -// NewOrLoadSyncRemoteKV constructs a [RemoteKV]. -// -// Parameters: -// - args[0] - ID of [E2e] object in tracker (int). -// - args[1] - A Javascript object that implements the functions on -// [RemoteKVCallbacks]. These will be the callbacks that are called for -// [bindings.RemoteStore] operations. -// - args[2] - A [RemoteStoreCallbacks]. This will be a structure the consumer -// implements. This acts as a wrapper around the remote storage API -// (e.g., Google Drive's API, DropBox's API, etc.). -// -// Returns a promise: -// - Resolves to a Javascript representation of the [RemoteKV] object. -// - Rejected with an error if initialising the remote KV fails. -func NewOrLoadSyncRemoteKV(_ js.Value, args []js.Value) any { - e2eID := args[0].Int() - remoteKvCallbacks := newRemoteKVCallbacks(args[1]) - remote := newRemoteStoreCallbacks(args[2]) - - promiseFn := func(resolve, reject func(args ...any) js.Value) { - api, err := - bindings.NewOrLoadSyncRemoteKV(e2eID, remoteKvCallbacks, remote) - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve(newRemoteKvJS(api)) - } - } - - return utils.CreatePromise(promiseFn) -} - -// Write writes a transaction to the remote and local store. -// -// Parameters: -// - args[0] - The key that this data will be written to (i.e., the device -// name, the channel name, etc.). Certain keys should follow a pattern and -// contain special characters (see [RemoteKV.GetList] for details) (string). -// - args[1] - The data that will be stored (i.e., state data) (Uint8Array). -// - args[2] - A Javascript object that implements the functions on -// [RemoteKVCallbacks]. This may be nil if you do not care about the network -// report. -// -// Returns a promise: -// - Resolves on success (void). -// - Rejected with an error if writing to the file fails. -func (rkv *RemoteKV) Write(_ js.Value, args []js.Value) any { - path := args[0].String() - data := utils.CopyBytesToGo(args[1]) - cb := newRemoteKVCallbacks(args[1]) - - promiseFn := func(resolve, reject func(args ...any) js.Value) { - err := rkv.api.Write(path, data, cb) - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve() - } - } - - return utils.CreatePromise(promiseFn) -} - -// Read retrieves the data stored in the underlying KV. Returns an error if the -// data at this key cannot be retrieved. -// -// Parameters: -// - args[0] - The path that this data will be written to (i.e., the device -// name) (string). -// -// Returns a promise: -// - Resolves to the file data (Uint8Array) -// - Rejected with an error if reading from the file fails. -func (rkv *RemoteKV) Read(_ js.Value, args []js.Value) any { - path := args[0].String() - - promiseFn := func(resolve, reject func(args ...any) js.Value) { - data, err := rkv.api.Read(path) - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve(utils.CopyBytesToJS(data)) - } - } - - return utils.CreatePromise(promiseFn) -} - -// GetList returns all entries for a path (or key) that contain the name -// parameter from the local store. -// -// For example, assuming the usage of the [sync.LocalStoreKeyDelimiter], if both -// "channels-123" and "channels-abc" are written to [RemoteKV], then -// GetList("channels") will retrieve the data for both channels. All data that -// contains no [sync.LocalStoreKeyDelimiter] can be retrieved using GetList(""). -// -// Parameters: -// - args[0] - Some prefix to a Write operation. If no prefix applies, simply -// use the empty string. (string). -// -// Returns: -// - The file data (Uint8Array) -// - Throws a TypeError if getting the list fails. -func (rkv *RemoteKV) GetList(_ js.Value, args []js.Value) any { - name := args[0].String() - - data, err := rkv.api.GetList(name) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil - } - - return utils.CopyBytesToJS(data) -} - -// RemoteStoreCallbacks wraps Javascript callbacks to adhere to the -// [bindings.RemoteStore] interface. -type RemoteStoreCallbacks struct { - read func(args ...any) js.Value - write func(args ...any) js.Value - getLastModified func(args ...any) js.Value - getLastWrite func(args ...any) js.Value -} - -// newRemoteStoreCallbacks maps the functions of the Javascript object matching -// [bindings.RemoteStore] to a RemoteStoreCallbacks. -func newRemoteStoreCallbacks(arg js.Value) *RemoteStoreCallbacks { - return &RemoteStoreCallbacks{ - read: utils.WrapCB(arg, "Read"), - write: utils.WrapCB(arg, "Write"), - getLastModified: utils.WrapCB(arg, "GetLastModified"), - getLastWrite: utils.WrapCB(arg, "GetLastWrite"), - } -} - -// Read reads from the provided file path and returns the data at that path. -// An error is returned if it failed to read the file. -// -// Parameters: -// - path - The file path to read from (string). -// -// Returns: -// - The file data (Uint8Array). -// - Catches any thrown errors (of type Error) and returns it as an error. -func (rsCB *RemoteStoreCallbacks) Read(path string) ([]byte, error) { - - fn := func() js.Value { return rsCB.read(path) } - v, err := utils.RunAndCatch(fn) - if err != nil { - return nil, err - } - return utils.CopyBytesToGo(v), err -} - -// Write writes to the file path the provided data. An error is returned if it -// fails to write to file. -// -// Parameters: -// - path - The file path to write to (string). -// - data - The file data to write (Uint8Array). -// -// Returns: -// - Catches any thrown errors (of type Error) and returns it as an error. -func (rsCB *RemoteStoreCallbacks) Write(path string, data []byte) error { - fn := func() js.Value { return rsCB.write(path, utils.CopyBytesToJS(data)) } - _, err := utils.RunAndCatch(fn) - return err -} - -// GetLastModified returns when the file at the given file path was last -// modified. If the implementation that adheres to this interface does not -// support this, [Write] or [Read] should be implemented to either write a -// separate timestamp file or add a prefix. -// -// Parameters: -// - path - The file path (string). -// -// Returns: -// - JSON of [bindings.RemoteStoreReport] (Uint8Array). -// - Catches any thrown errors (of type Error) and returns it as an error. -func (rsCB *RemoteStoreCallbacks) GetLastModified(path string) ([]byte, error) { - fn := func() js.Value { return rsCB.getLastModified(path) } - v, err := utils.RunAndCatch(fn) - if err != nil { - return nil, err - } - return utils.CopyBytesToGo(v), err -} - -// GetLastWrite retrieves the most recent successful write operation that was -// received by [RemoteStoreFileSystem]. -// -// Returns: -// - JSON of [bindings.RemoteStoreReport] (Uint8Array). -// - Catches any thrown errors (of type Error) and returns it as an error. -func (rsCB *RemoteStoreCallbacks) GetLastWrite() ([]byte, error) { - fn := func() js.Value { return rsCB.getLastWrite() } - v, err := utils.RunAndCatch(fn) - if err != nil { - return nil, err - } - return utils.CopyBytesToGo(v), err -} - -// RemoteKVCallbacks wraps Javascript callbacks to adhere to the -// [bindings.RemoteKVCallbacks] interface. -type RemoteKVCallbacks struct { - keyUpdated func(args ...any) js.Value - remoteStoreResult func(args ...any) js.Value -} - -// newRemoteKVCallbacks maps the functions of the Javascript object matching -// [bindings.RemoteKVCallbacks] to a RemoteKVCallbacks. -func newRemoteKVCallbacks(arg js.Value) *RemoteKVCallbacks { - return &RemoteKVCallbacks{ - keyUpdated: utils.WrapCB(arg, "KeyUpdated"), - remoteStoreResult: utils.WrapCB(arg, "RemoteStoreResult"), - } -} - -// KeyUpdated is the callback to be called any time a key is updated by another -// device tracked by the [RemoteKV] store. -// -// Parameters: -// - key - (string). -// - oldVal - (Uint8Array). -// - newVal - (Uint8Array). -// - updated - (Boolean) -func (rkvCB *RemoteKVCallbacks) KeyUpdated( - key string, oldVal, newVal []byte, updated bool) { - rkvCB.keyUpdated( - key, utils.CopyBytesToJS(oldVal), utils.CopyBytesToJS(newVal), updated) -} - -// RemoteStoreResult is called to report network save results after the key has -// been updated locally. -// -// NOTE: Errors originate from the authentication and writing code in regard to -// remote which is handled by the user of this API. As a result, this callback -// provides no information in simple implementations. -// -// Parameters: -// - remoteStoreReport - JSON of [bindings.RemoteStoreReport] (Uint8Array). -func (rkvCB *RemoteKVCallbacks) RemoteStoreResult(remoteStoreReport []byte) { - rkvCB.remoteStoreResult(utils.CopyBytesToJS(remoteStoreReport)) -}