diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2947dbbb867bfff780f68840ef927b7bd457347..f70c6360c2609120af3b32b7fad230c47e90d07e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,12 +38,12 @@ wasm-test: - tags script: - export PATH=/root/go/bin:$PATH - - echo > utils/utils_js.s - go mod vendor - unset SSH_PRIVATE_KEY - unset $(env | grep '=' | awk -F= '{print $1}' | grep -v PATH | grep -v GO | grep -v HOME) - - echo "WASM TESTS DISABLED FOR XX-4522, but will run them just so you can see output" - - GOOS=js GOARCH=wasm go test ./... -v || true + - rm vendor/gitlab.com/elixxir/wasm-utils/exception/throw_js.s + - mv vendor/gitlab.com/elixxir/wasm-utils/exception/throws.dev vendor/gitlab.com/elixxir/wasm-utils/exception/throws.go + - GOOS=js GOARCH=wasm go test ./... -v build: stage: build @@ -69,9 +69,11 @@ build-workers: - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-channelsIndexedDkWorker.wasm ./indexedDb/impl/channels/... - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/... - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-logFileWorker.wasm ./logging/workerThread/... + - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-stateIndexedDbWorker.wasm ./indexedDb/impl/state/... - cp indexedDb/impl/channels/channelsIndexedDbWorker.js release/ - cp indexedDb/impl/dm/dmIndexedDbWorker.js release/ - cp logging/workerThread/logFileWorker.js release/ + - cp indexedDb/impl/state/stateIndexedDbWorker.js release/ artifacts: paths: - release/ @@ -112,9 +114,11 @@ combine-artifacts: - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-channelsIndexedDkWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-channelsIndexedDkWorker.wasm' - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-dmIndexedDkWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-dmIndexedDkWorker.wasm' - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-logFileWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-logFileWorker.wasm' + - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-stateIndexedDbWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-stateIndexedDbWorker.wasm' - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/channelsIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/channelsIndexedDbWorker.js' - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/dmIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/dmIndexedDbWorker.js' - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/logFileWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/logFileWorker.js' + - 'curl --fail --location --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/stateIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/stateIndexedDbWorker.js' - ls release artifacts: paths: diff --git a/Makefile b/Makefile index f8c143a6a24f68f6b70d2bc64cc6028b56f4c9af..cd183023d09da89d457b2f7a4ec08d33ab75db15 100644 --- a/Makefile +++ b/Makefile @@ -11,18 +11,20 @@ build: GOOS=js GOARCH=wasm go build ./... update_release: + GOFLAGS="" go get gitlab.com/elixxir/wasm-utils@release GOFLAGS="" go get gitlab.com/xx_network/primitives@release GOFLAGS="" go get gitlab.com/elixxir/primitives@release GOFLAGS="" go get gitlab.com/xx_network/crypto@release GOFLAGS="" go get gitlab.com/elixxir/crypto@release - GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@release + GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@project/HavenBeta update_master: - GOFLAGS="" go get -d gitlab.com/elixxir/client@master - GOFLAGS="" go get gitlab.com/elixxir/crypto@master + GOFLAGS="" go get gitlab.com/elixxir/wasm-utils@master + GOFLAGS="" go get gitlab.com/xx_network/primitives@master GOFLAGS="" go get gitlab.com/elixxir/primitives@master GOFLAGS="" go get gitlab.com/xx_network/crypto@master - GOFLAGS="" go get gitlab.com/xx_network/primitives@master + GOFLAGS="" go get gitlab.com/elixxir/crypto@master + GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@master binary: GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk.wasm main.go @@ -30,15 +32,21 @@ binary: worker_binaries: GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-channelsIndexedDkWorker.wasm ./indexedDb/impl/channels/... GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/... + GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-stateIndexedDkWorker.wasm ./indexedDb/impl/state/... GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-logFileWorker.wasm ./logging/workerThread/... binaries: binary worker_binaries +wasmException = "vendor/gitlab.com/elixxir/wasm-utils/exception" + wasm_tests: - cp utils/utils_js.s utils/utils_js.s.bak - > utils/utils_js.s + cp $(wasmException)/throw_js.s $(wasmException)/throw_js.s.bak + cp $(wasmException)/throws.go $(wasmException)/throws.go.bak + > $(wasmException)/throw_js.s + cp $(wasmException)/throws.dev $(wasmException)/throws.go -GOOS=js GOARCH=wasm go test -v ./... - mv utils/utils_js.s.bak utils/utils_js.s + mv $(wasmException)/throw_js.s.bak $(wasmException)/throw_js.s + mv $(wasmException)/throws.go.bak $(wasmException)/throws.go go_tests: go test ./... -v diff --git a/README.md b/README.md index d1f59cd4392fce9f8b2b541862e69fa684400039..a898480b75ee28ead215556ad146c7e9a3cdf842 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ global.Go = class { go: { // ... // func Throw(exception string, message string) - 'gitlab.com/elixxir/xxdk-wasm/utils.throw': (sp) => { + 'gitlab.com/elixxir/wasm-utils/utils.throw': (sp) => { const exception = loadString(sp + 8) const message = loadString(sp + 24) throw globalThis[exception](message) diff --git a/go.mod b/go.mod index 451c29d33b6508154a2f659053bfad943c183e30..55fa3b9d967a383db414693e960113256ec565e5 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,15 @@ require ( github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/hack-pad/go-indexeddb v0.2.0 github.com/pkg/errors v0.9.1 + github.com/spf13/cobra v1.7.0 github.com/spf13/jwalterweatherman v1.1.0 - gitlab.com/elixxir/client/v4 v4.6.4-0.20230426210017-366b4270492e - gitlab.com/elixxir/crypto v0.0.7-0.20230424221508-14c052d4b967 + github.com/stretchr/testify v1.8.2 + gitlab.com/elixxir/client/v4 v4.6.4-0.20230526193945-9f195a236f77 + gitlab.com/elixxir/crypto v0.0.7-0.20230526183834-62f8f49617bc gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c + gitlab.com/elixxir/wasm-utils v0.0.0-20230522231408-a43b2c1481b2 gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd - gitlab.com/xx_network/primitives v0.0.4-0.20230310205521-c440e68e34c4 + gitlab.com/xx_network/primitives v0.0.4-0.20230522171102-940cdd68e516 golang.org/x/crypto v0.5.0 ) @@ -22,6 +25,7 @@ require ( github.com/badoux/checkmail v1.2.1 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cloudflare/circl v1.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/elliotchance/orderedmap v1.4.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect @@ -31,12 +35,13 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.9 // indirect github.com/klauspost/cpuid/v2 v2.1.0 // indirect + github.com/kr/pretty v0.3.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-sqlite3 v1.14.15 // indirect @@ -48,13 +53,13 @@ require ( github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.2 // indirect github.com/pkg/profile v1.6.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.8.2 // indirect github.com/sethvargo/go-diceware v0.3.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.12.0 // indirect github.com/subosito/gotenv v1.4.0 // indirect @@ -63,8 +68,8 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/zeebo/blake3 v0.2.3 // indirect gitlab.com/elixxir/bloomfilter v0.0.0-20230322223210-fa84f6842de8 // indirect - gitlab.com/elixxir/comms v0.0.4-0.20230322130755-e59b16bce601 // indirect - gitlab.com/elixxir/ekv v0.2.2 // indirect + gitlab.com/elixxir/comms v0.0.4-0.20230519211512-4a998f4b0938 // indirect + gitlab.com/elixxir/ekv v0.3.1-0.20230525213559-f9da13f4fce1 // indirect gitlab.com/xx_network/comms v0.0.4-0.20230214180029-5387fb85736d // indirect gitlab.com/xx_network/ring v0.0.3-0.20220902183151-a7d3b15bc981 // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect diff --git a/go.sum b/go.sum index f6565c5bab2e9f653cad6d914e6912754fcd4198..b3aa49e12dccd8ce395670f167ca59d718d29bca 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,7 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -266,8 +267,9 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= @@ -303,8 +305,12 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ktr0731/grpc-test v0.1.12 h1:Yha+zH2hB48huOfbsEMfyG7FeHCrVWq4fYmHfr3iH3U= github.com/ktr0731/grpc-web-go-client v0.2.8 h1:nUf9p+YWirmFwmH0mwtAWhuXvzovc+/3C/eAY2Fshnk= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= @@ -422,6 +428,8 @@ github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= @@ -450,8 +458,8 @@ github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcD github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -505,28 +513,34 @@ 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-20230322223210-fa84f6842de8 h1:uAFCyBkXprQoPkcDDfxXtaMyL5x+xSGrAWzR907xROQ= gitlab.com/elixxir/bloomfilter v0.0.0-20230322223210-fa84f6842de8/go.mod h1:1X8gRIAPDisS3W6Vtr/ymiUmZMJUIwDV1o5DEOo/pzw= -gitlab.com/elixxir/client/v4 v4.6.3 h1:oUsm5cn2Vnfqz+xwGYKrqFkPNN3sDAyp00EPGhUIA5E= -gitlab.com/elixxir/client/v4 v4.6.3/go.mod h1:G+lN+LvQPGcm5BQnrhnqT1xiRIAzH3OffAM+5oI9SUg= -gitlab.com/elixxir/client/v4 v4.6.4-0.20230426210017-366b4270492e h1:Fvy4IBGtZLmqFXY78ucML8JiQhHCijpmp1uyQRSs8Ug= -gitlab.com/elixxir/client/v4 v4.6.4-0.20230426210017-366b4270492e/go.mod h1:8r/PC1nVifMmOYcK1ko0UkaAgfi+J/OVxSBj6LkwrO4= -gitlab.com/elixxir/comms v0.0.4-0.20230322130755-e59b16bce601 h1:l9ZVDOXf0fvbFnNXWmwnsEIvUIUL5fy3mFexrYg8dx4= -gitlab.com/elixxir/comms v0.0.4-0.20230322130755-e59b16bce601/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q= -gitlab.com/elixxir/crypto v0.0.7-0.20230413162806-a99ec4bfea32 h1:Had0F7rMPgJJ2BUZoFNgeJq33md9RpV15nvd08Uxdzc= -gitlab.com/elixxir/crypto v0.0.7-0.20230413162806-a99ec4bfea32/go.mod h1:/SLOlvkYVVJf6IU+vEjMLnS7cjjcoTlPV45g6tv6INc= -gitlab.com/elixxir/crypto v0.0.7-0.20230424221508-14c052d4b967 h1:yKGoNe9xtHROwbep7yGYhTvbxm4cSRycduAkEArTE9s= -gitlab.com/elixxir/crypto v0.0.7-0.20230424221508-14c052d4b967/go.mod h1:/SLOlvkYVVJf6IU+vEjMLnS7cjjcoTlPV45g6tv6INc= -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/ekv v0.2.2 h1:hzb3JLTFJXETaSvWoK1xJ89K6W00uFprcoAdogPZCEM= -gitlab.com/elixxir/ekv v0.2.2/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230525191042-3795029e4315 h1:rOgY9KPwq0wCQGM2VWzHQLidUwo/igqEbHvrfrX14NE= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230525191042-3795029e4315/go.mod h1:1+FU4spF6kwSA84AnFq0i6j4jsAICuvVfp6ACh00K0U= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230526074923-c0fb2a45c312 h1:yUyz1L/rzodZxUU45CeH7mfMfmKAs706+s2j2ZGUSwE= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230526074923-c0fb2a45c312/go.mod h1:fegbuF1/6a+H3QgsoMG8teLnyuKtDxkELMw8pn5WlZ8= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230526185452-5da5d4f474f6 h1:qAIeh68suM7rLJ5RHt1AvZoYmehIwPLGeDzm6SMc0R4= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230526185452-5da5d4f474f6/go.mod h1:0fEqbELJdwTNRsrvgecZbGsJSX2TqHUst/LSBKo/cL0= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230526193945-9f195a236f77 h1:qCxO7ZuUGXh54iCi8hlzl3OBBI+B1MzngCZAWCv+S9c= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230526193945-9f195a236f77/go.mod h1:0fEqbELJdwTNRsrvgecZbGsJSX2TqHUst/LSBKo/cL0= +gitlab.com/elixxir/comms v0.0.4-0.20230519211512-4a998f4b0938 h1:f27+QUFiGWrprKm+fstOg3ABkYLpWcZi3+8Lf5eDnqY= +gitlab.com/elixxir/comms v0.0.4-0.20230519211512-4a998f4b0938/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q= +gitlab.com/elixxir/crypto v0.0.7-0.20230522162218-45433d877235 h1:0BySdXTzRWxzH8k5RiNNMmmn2lpuQWLVcDDA/7ehyqc= +gitlab.com/elixxir/crypto v0.0.7-0.20230522162218-45433d877235/go.mod h1:IYInxKr5Q7EH3oNhg1QX1/sTTRNi7L0JkcyfdRegoio= +gitlab.com/elixxir/crypto v0.0.7-0.20230526183834-62f8f49617bc h1:Rl8q37axi4XVuuDfXP+bYc9iAcVb3O9jyYWuQTV5+Z8= +gitlab.com/elixxir/crypto v0.0.7-0.20230526183834-62f8f49617bc/go.mod h1:IYInxKr5Q7EH3oNhg1QX1/sTTRNi7L0JkcyfdRegoio= +gitlab.com/elixxir/ekv v0.3.1-0.20230525165450-f444c687504b h1:hf28yepO93tCacx1bUAh8vVFkBUEuBaJhOjifBxEQK4= +gitlab.com/elixxir/ekv v0.3.1-0.20230525165450-f444c687504b/go.mod h1:EMaUQrsOxvEPQ0/8V/PSkGqFmEC2axBG/uqY0oW2uJM= +gitlab.com/elixxir/ekv v0.3.1-0.20230525213559-f9da13f4fce1 h1:8XBo6QQBXXGCTrgXHFuqPL21mROLKLAoO3X9xR5TwA0= +gitlab.com/elixxir/ekv v0.3.1-0.20230525213559-f9da13f4fce1/go.mod h1:UStTZ9d1UVn9Ahyb49lrbPKyr/Wb8xFWqMXbDgIqQhE= gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c h1:muG8ff95woeVVwQoJHCEclxBFB22lc7EixPylEkYDRU= gitlab.com/elixxir/primitives v0.0.3-0.20230214180039-9a25e2d3969c/go.mod h1:phun4PLkHJA6wcL4JIhhxZztrmCyJHWPNppBP3DUD2Y= +gitlab.com/elixxir/wasm-utils v0.0.0-20230522231408-a43b2c1481b2 h1:GQb350yPBkWRkPRgNSVFF0ZZDOAlXWIKQBI/1Ff6biU= +gitlab.com/elixxir/wasm-utils v0.0.0-20230522231408-a43b2c1481b2/go.mod h1:wB7Vh/7LWUm8wYRBSd+6lxfpk4CnDaHTkLCIVKfL2TA= gitlab.com/xx_network/comms v0.0.4-0.20230214180029-5387fb85736d h1:AZf2h0fxyO1KxhZPP9//jG3Swb2BcuKbxtNXJgooLss= gitlab.com/xx_network/comms v0.0.4-0.20230214180029-5387fb85736d/go.mod h1:8cwPyH6G8C4qf/U5KDghn1ksOh79MrNqthjKDrfvbXY= gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd h1:IleH6U5D/c2zF6YL/z3cBKqBPnI5ApNMCtU7ia4t228= gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd/go.mod h1:PPPaFoY5Ze1qft9D0a24UHAwlvWEc2GbraihXvKYkf4= -gitlab.com/xx_network/primitives v0.0.4-0.20230310205521-c440e68e34c4 h1:g8dsLA3tMjoix/9kZl+ELxGt/cTuuPopqUagawPwYpk= -gitlab.com/xx_network/primitives v0.0.4-0.20230310205521-c440e68e34c4/go.mod h1:ABtt5oK+Sl1Q9l3qWK9efxmLKtNMSskpIjbe6IvB9sQ= +gitlab.com/xx_network/primitives v0.0.4-0.20230522171102-940cdd68e516 h1:+mhYGiDuVGlTzZMvM7bAybGU85oSXffwIgYxeeBkJmE= +gitlab.com/xx_network/primitives v0.0.4-0.20230522171102-940cdd68e516/go.mod h1:ABtt5oK+Sl1Q9l3qWK9efxmLKtNMSskpIjbe6IvB9sQ= 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= @@ -915,6 +929,7 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/indexedDb/impl/channels/callbacks.go b/indexedDb/impl/channels/callbacks.go index df560535a779f2e6c355c6e49a66d4b45c2047e2..2a901597117fac367e360ee2f80a2567fcab036f 100644 --- a/indexedDb/impl/channels/callbacks.go +++ b/indexedDb/impl/channels/callbacks.go @@ -10,8 +10,9 @@ package main import ( - "crypto/ed25519" "encoding/json" + "time" + "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/channels" @@ -24,7 +25,6 @@ import ( "gitlab.com/elixxir/xxdk-wasm/worker" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/primitives/id" - "time" ) var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0} @@ -32,24 +32,24 @@ var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0} // manager handles the event model and the message callbacks, which is used to // send information between the event model and the main thread. type manager struct { - mh *worker.ThreadManager + wtm *worker.ThreadManager model channels.EventModel } // registerCallbacks registers all the reception callbacks to manage messages // from the main thread for the channels.EventModel. func (m *manager) registerCallbacks() { - m.mh.RegisterCallback(wChannels.NewWASMEventModelTag, m.newWASMEventModelCB) - m.mh.RegisterCallback(wChannels.JoinChannelTag, m.joinChannelCB) - m.mh.RegisterCallback(wChannels.LeaveChannelTag, m.leaveChannelCB) - m.mh.RegisterCallback(wChannels.ReceiveMessageTag, m.receiveMessageCB) - m.mh.RegisterCallback(wChannels.ReceiveReplyTag, m.receiveReplyCB) - m.mh.RegisterCallback(wChannels.ReceiveReactionTag, m.receiveReactionCB) - m.mh.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUUIDCB) - m.mh.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIDCB) - m.mh.RegisterCallback(wChannels.GetMessageTag, m.getMessageCB) - m.mh.RegisterCallback(wChannels.DeleteMessageTag, m.deleteMessageCB) - m.mh.RegisterCallback(wChannels.MuteUserTag, m.muteUserCB) + m.wtm.RegisterCallback(wChannels.NewWASMEventModelTag, m.newWASMEventModelCB) + m.wtm.RegisterCallback(wChannels.JoinChannelTag, m.joinChannelCB) + m.wtm.RegisterCallback(wChannels.LeaveChannelTag, m.leaveChannelCB) + m.wtm.RegisterCallback(wChannels.ReceiveMessageTag, m.receiveMessageCB) + m.wtm.RegisterCallback(wChannels.ReceiveReplyTag, m.receiveReplyCB) + m.wtm.RegisterCallback(wChannels.ReceiveReactionTag, m.receiveReactionCB) + m.wtm.RegisterCallback(wChannels.UpdateFromUUIDTag, m.updateFromUUIDCB) + m.wtm.RegisterCallback(wChannels.UpdateFromMessageIDTag, m.updateFromMessageIDCB) + m.wtm.RegisterCallback(wChannels.GetMessageTag, m.getMessageCB) + m.wtm.RegisterCallback(wChannels.DeleteMessageTag, m.deleteMessageCB) + m.wtm.RegisterCallback(wChannels.MuteUserTag, m.muteUserCB) } // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty @@ -71,8 +71,7 @@ func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) { "failed to JSON unmarshal Cipher from main thread: %+v", err) } - m.model, err = NewWASMEventModel(msg.DatabaseName, encryption, - m.messageReceivedCallback, m.deletedMessageCallback, m.mutedUserCallback) + m.model, err = NewWASMEventModel(msg.DatabaseName, encryption, m) if err != nil { return []byte(err.Error()), nil } @@ -80,47 +79,12 @@ func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) { return []byte{}, nil } -// messageReceivedCallback sends calls to the channels.MessageReceivedCallback -// in the main thread. -// -// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type. -func (m *manager) messageReceivedCallback( - uuid uint64, channelID *id.ID, update bool) { - // Package parameters for sending - msg := &wChannels.MessageReceivedCallbackMessage{ - UUID: uuid, - ChannelID: channelID, - Update: update, - } - data, err := json.Marshal(msg) - if err != nil { - jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err) - return - } - - // Send it to the main thread - m.mh.SendMessage(wChannels.MessageReceivedCallbackTag, data) -} - -// deletedMessageCallback sends calls to the channels.DeletedMessageCallback in -// the main thread. -// -// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type. -func (m *manager) deletedMessageCallback(messageID message.ID) { - m.mh.SendMessage(wChannels.DeletedMessageCallbackTag, messageID.Marshal()) -} - -// mutedUserCallback sends calls to the channels.MutedUserCallback in the main -// thread. -// -// storeEncryptionStatus adhere to the channels.MessageReceivedCallback type. -func (m *manager) mutedUserCallback( - channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) { +// EventUpdate implements [bindings.ChannelUICallbacks.EventUpdate]. +func (m *manager) EventUpdate(eventType int64, jsonData []byte) { // Package parameters for sending - msg := &wChannels.MuteUserMessage{ - ChannelID: channelID, - PubKey: pubKey, - Unmute: unmute, + msg := &wChannels.EventUpdateCallbackMessage{ + EventType: eventType, + JsonData: jsonData, } data, err := json.Marshal(msg) if err != nil { @@ -129,7 +93,7 @@ func (m *manager) mutedUserCallback( } // Send it to the main thread - m.mh.SendMessage(wChannels.MutedUserCallbackTag, data) + m.wtm.SendMessage(wChannels.EventUpdateCallbackTag, data) } // joinChannelCB is the callback for wasmModel.JoinChannel. Always returns nil; @@ -368,7 +332,12 @@ func (m *manager) muteUserCB(data []byte) ([]byte, error) { "failed to JSON unmarshal %T from main thread: %+v", msg, err) } - m.model.MuteUser(msg.ChannelID, msg.PubKey, msg.Unmute) + channelID := id.ID{} + err = channelID.UnmarshalJSON(msg.ChannelID) + if err != nil { + return nil, err + } + m.model.MuteUser(&channelID, msg.PubKey, msg.Unmute) return nil, nil } diff --git a/indexedDb/impl/channels/channelsIndexedDbWorker.js b/indexedDb/impl/channels/channelsIndexedDbWorker.js index 9e69bdd70eddebc9f82b23d04d823221ad3c1622..c109cba89735425f23c0d4d436532a3e3eb9a852 100644 --- a/indexedDb/impl/channels/channelsIndexedDbWorker.js +++ b/indexedDb/impl/channels/channelsIndexedDbWorker.js @@ -7,11 +7,15 @@ importScripts('wasm_exec.js'); +const isReady = new Promise((resolve) => { + self.onWasmInitialized = resolve; +}); + const go = new Go(); const binPath = 'xxdk-channelsIndexedDkWorker.wasm' -WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => { +WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); - LogLevel(1); + await isReady; }).catch((err) => { console.error(err); }); \ No newline at end of file diff --git a/indexedDb/impl/channels/fileTransferImpl.go b/indexedDb/impl/channels/fileTransferImpl.go index d75dc78b6dddd5825d7c1b157dddadd75d62698d..4e30a7f91933d7cae8bc76a0efd06000be5dc3ef 100644 --- a/indexedDb/impl/channels/fileTransferImpl.go +++ b/indexedDb/impl/channels/fileTransferImpl.go @@ -15,8 +15,8 @@ import ( "gitlab.com/elixxir/client/v4/channels" cft "gitlab.com/elixxir/client/v4/channelsFileTransfer" "gitlab.com/elixxir/crypto/fileTransfer" + "gitlab.com/elixxir/wasm-utils/utils" "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" - "gitlab.com/elixxir/xxdk-wasm/utils" "strings" "time" ) diff --git a/indexedDb/impl/channels/implementation.go b/indexedDb/impl/channels/implementation.go index 415aca68d4e0c4e9dd7269a055bd9c3322770922..e755acbafb3f750e577305fb9e241c537f30febe 100644 --- a/indexedDb/impl/channels/implementation.go +++ b/indexedDb/impl/channels/implementation.go @@ -21,28 +21,24 @@ import ( "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/client/v4/channels" "gitlab.com/elixxir/client/v4/cmix/rounds" cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/message" + "gitlab.com/elixxir/wasm-utils/utils" "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" - wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels" - "gitlab.com/elixxir/xxdk-wasm/utils" "gitlab.com/xx_network/primitives/id" ) -// wasmModel implements [channels.EventModel] interface, which uses the channels -// system passed an object that adheres to in order to get events on the -// channel. +// wasmModel implements [channels.EventModel] interface backed by IndexedDb. // NOTE: This model is NOT thread safe - it is the responsibility of the // caller to ensure that its methods are called sequentially. type wasmModel struct { - db *idb.Database - cipher cryptoChannel.Cipher - receivedMessageCB wChannels.MessageReceivedCallback - deletedMessageCB wChannels.DeletedMessageCallback - mutedUserCB wChannels.MutedUserCallback + db *idb.Database + cipher cryptoChannel.Cipher + eventUpdate func(eventType int64, jsonMarshallable any) } // JoinChannel is called whenever a channel is joined locally. @@ -121,19 +117,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) @@ -161,8 +157,10 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID, } } + channelIDBytes := channelID.Marshal() + msgToInsert := buildMessage( - channelID.Marshal(), messageID.Bytes(), nil, nickname, + channelIDBytes, messageID.Bytes(), nil, nickname, textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, false, hidden, status) @@ -172,7 +170,11 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID, return 0 } - go w.receivedMessageCB(uuid, channelID, false) + go w.eventUpdate(bindings.MessageReceived, bindings.MessageReceivedJson{ + Uuid: int64(uuid), + ChannelID: channelID, + Update: false, + }) return uuid } @@ -199,7 +201,9 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID, } } - msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(), + channelIDBytes := channelID.Marshal() + + msgToInsert := buildMessage(channelIDBytes, messageID.Bytes(), replyTo.Bytes(), nickname, textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, hidden, false, status) @@ -208,7 +212,12 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID, jww.ERROR.Printf("Failed to receive reply: %+v", err) return 0 } - go w.receivedMessageCB(uuid, channelID, false) + + go w.eventUpdate(bindings.MessageReceived, bindings.MessageReceivedJson{ + Uuid: int64(uuid), + ChannelID: channelID, + Update: false, + }) return uuid } @@ -235,8 +244,9 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID, } } + channelIDBytes := channelID.Marshal() msgToInsert := buildMessage( - channelID.Marshal(), messageID.Bytes(), reactionTo.Bytes(), nickname, + channelIDBytes, messageID.Bytes(), reactionTo.Bytes(), nickname, textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, false, hidden, status) @@ -245,7 +255,12 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID, jww.ERROR.Printf("Failed to receive reaction: %+v", err) return 0 } - go w.receivedMessageCB(uuid, channelID, false) + + go w.eventUpdate(bindings.MessageReceived, bindings.MessageReceivedJson{ + Uuid: int64(uuid), + ChannelID: channelID, + Update: false, + }) return uuid } @@ -392,9 +407,17 @@ func (w *wasmModel) updateMessage(currentMsg *Message, messageID *message.ID, if err != nil { return 0, err } - channelID := &id.ID{} - copy(channelID[:], currentMsg.ChannelID) - go w.receivedMessageCB(uuid, channelID, true) + + channelID, err := id.Unmarshal(currentMsg.ChannelID) + if err != nil { + return 0, err + } + + go w.eventUpdate(bindings.MessageReceived, bindings.MessageReceivedJson{ + Uuid: int64(uuid), + ChannelID: channelID, + Update: true, + }) return uuid, nil } @@ -415,7 +438,8 @@ func (w *wasmModel) upsertMessage(msg *Message) (uint64, error) { // Store message to database msgIdObj, err := impl.Put(w.db, messageStoreName, messageObj) if err != nil { - return 0, errors.Errorf("Unable to put Message: %+v", err) + return 0, errors.Errorf("Unable to put Message: %+v\n%s", + err, newMessageJson) } uuid := msgIdObj.Int() @@ -491,7 +515,8 @@ func (w *wasmModel) DeleteMessage(messageID message.ID) error { return err } - go w.deletedMessageCB(messageID) + go w.eventUpdate(bindings.MessageDeleted, + bindings.MessageDeletedJson{MessageID: messageID}) return nil } @@ -499,7 +524,12 @@ func (w *wasmModel) DeleteMessage(messageID message.ID) error { // MuteUser is called whenever a user is muted or unmuted. func (w *wasmModel) MuteUser( channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) { - go w.mutedUserCB(channelID, pubKey, unmute) + + go w.eventUpdate(bindings.UserMuted, bindings.UserMutedJson{ + ChannelID: channelID, + PubKey: pubKey, + Unmute: unmute, + }) } // valueToMessage is a helper for converting js.Value to Message. diff --git a/indexedDb/impl/channels/implementation_test.go b/indexedDb/impl/channels/implementation_test.go index 4b22b8300f5549a5dc2f3902f7ed95277777555f..f596065dcc47aa39e3b5aeda7b97a1f389071977 100644 --- a/indexedDb/impl/channels/implementation_test.go +++ b/indexedDb/impl/channels/implementation_test.go @@ -11,12 +11,9 @@ package main import ( "bytes" - "crypto/ed25519" "encoding/json" "errors" "fmt" - cft "gitlab.com/elixxir/client/v4/channelsFileTransfer" - "gitlab.com/elixxir/crypto/fileTransfer" "os" "strconv" "testing" @@ -24,14 +21,17 @@ import ( "github.com/hack-pad/go-indexeddb/idb" jww "github.com/spf13/jwalterweatherman" + "github.com/stretchr/testify/require" "gitlab.com/elixxir/client/v4/channels" + cft "gitlab.com/elixxir/client/v4/channelsFileTransfer" "gitlab.com/elixxir/client/v4/cmix/rounds" cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/fileTransfer" "gitlab.com/elixxir/crypto/message" + "gitlab.com/elixxir/wasm-utils/storage" "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" - "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" @@ -42,15 +42,14 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func dummyReceivedMessageCB(uint64, *id.ID, bool) {} -func dummyDeletedMessageCB(message.ID) {} -func dummyMutedUserCB(*id.ID, ed25519.PublicKey, bool) {} +type dummyCbs struct{} + +func (c *dummyCbs) EventUpdate(int64, []byte) {} // Happy path test for receiving, updating, getting, and deleting a File. func TestWasmModel_ReceiveFile(t *testing.T) { testString := "TestWasmModel_ReceiveFile" - m, err := newWASMModel(testString, nil, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + m, err := newWASMModel(testString, nil, &dummyCbs{}) if err != nil { t.Fatal(err) } @@ -137,7 +136,7 @@ func TestWasmModel_GetMessage(t *testing.T) { testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + &dummyCbs{}) if err != nil { t.Fatal(err) } @@ -167,8 +166,7 @@ func TestWasmModel_DeleteMessage(t *testing.T) { storage.GetLocalStorage().Clear() testString := "TestWasmModel_DeleteMessage" testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) - eventModel, err := newWASMModel(testString, nil, dummyReceivedMessageCB, - dummyDeletedMessageCB, dummyMutedUserCB) + eventModel, err := newWASMModel(testString, nil, &dummyCbs{}) if err != nil { t.Fatal(err) } @@ -225,13 +223,17 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { testMsgId := message.DeriveChannelMessageID( &id.ID{1}, 0, []byte(testString)) eventModel, err2 := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + &dummyCbs{}) if err2 != nil { t.Fatal(err) } + cid, err := id.NewRandomID(csprng.NewSystemRNG(), + id.DummyUser.GetType()) + require.NoError(t, err) + // Store a test message - testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, + testMsg := buildMessage(cid.Bytes(), testMsgId.Bytes(), nil, testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) uuid, err2 := eventModel.upsertMessage(testMsg) @@ -292,8 +294,7 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { } t.Run("Test_wasmModel_JoinChannel_LeaveChannel"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() - eventModel, err2 := newWASMModel("test", c, dummyReceivedMessageCB, - dummyDeletedMessageCB, dummyMutedUserCB) + eventModel, err2 := newWASMModel("test", c, &dummyCbs{}) if err2 != nil { t.Fatal(err2) } @@ -347,7 +348,7 @@ func Test_wasmModel_UUIDTest(t *testing.T) { storage.GetLocalStorage().Clear() testString := "testHello" + cs eventModel, err2 := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + &dummyCbs{}) if err2 != nil { t.Fatal(err2) } @@ -394,7 +395,7 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) { t.Run(testString, func(t *testing.T) { storage.GetLocalStorage().Clear() eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + &dummyCbs{}) if err != nil { t.Fatal(err) } @@ -443,7 +444,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { totalMessages := 10 expectedMessages := 5 eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + &dummyCbs{}) if err != nil { t.Fatal(err) } @@ -514,7 +515,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { storage.GetLocalStorage().Clear() testString := fmt.Sprintf("test_receiveHelper_UniqueIndex_%d", i) eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + &dummyCbs{}) if err != nil { t.Fatal(err) } diff --git a/indexedDb/impl/channels/init.go b/indexedDb/impl/channels/init.go index 363440d541029007a6c167d94f8a1fcd66edf4fd..48f5359f9b61c67b42533a42fa37cec4c3c917e7 100644 --- a/indexedDb/impl/channels/init.go +++ b/indexedDb/impl/channels/init.go @@ -10,49 +10,46 @@ package main import ( + "encoding/json" "syscall/js" "github.com/hack-pad/go-indexeddb/idb" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/client/v4/channels" cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" - wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels" ) // currentVersion is the current version of the IndexedDb runtime. Used for // migration purposes. -const currentVersion uint = 2 +const currentVersion uint = 1 // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel. // The name should be a base64 encoding of the users public key. Returns the // EventModel based on IndexedDb and the database name as reported by IndexedDb. func NewWASMEventModel(databaseName string, encryption cryptoChannel.Cipher, - messageReceivedCB wChannels.MessageReceivedCallback, - deletedMessageCB wChannels.DeletedMessageCallback, - mutedUserCB wChannels.MutedUserCallback) (channels.EventModel, error) { - return newWASMModel(databaseName, encryption, messageReceivedCB, - deletedMessageCB, mutedUserCB) + uiCallbacks bindings.ChannelUICallbacks) (channels.EventModel, error) { + return newWASMModel(databaseName, encryption, uiCallbacks) } // newWASMModel creates the given [idb.Database] and returns a wasmModel. func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, - messageReceivedCB wChannels.MessageReceivedCallback, - deletedMessageCB wChannels.DeletedMessageCallback, - mutedUserCB wChannels.MutedUserCallback) (*wasmModel, error) { + uiCallbacks bindings.ChannelUICallbacks) (*wasmModel, error) { // Attempt to open database object ctx, cancel := impl.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) + jww.INFO.Printf("IndexDb version for %s is current: v%d", + databaseName, newVersion) return nil } - jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d", - oldVersion, newVersion) + jww.INFO.Printf("IndexDb upgrade required for %s: v%d -> v%d", + databaseName, oldVersion, newVersion) if oldVersion == 0 && newVersion >= 1 { err := v1Upgrade(db) @@ -62,14 +59,6 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, oldVersion = 1 } - if oldVersion == 1 && newVersion >= 2 { - err := v2Upgrade(db) - if err != nil { - return err - } - oldVersion = 2 - } - // if oldVersion == 1 && newVersion >= 2 { v2Upgrade(), oldVersion = 2 } return nil }) @@ -81,16 +70,22 @@ 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, - cipher: encryption, - receivedMessageCB: messageReceivedCB, - deletedMessageCB: deletedMessageCB, - mutedUserCB: mutedUserCB, + db: db, + cipher: encryption, + eventUpdate: func(eventType int64, jsonMarshallable any) { + data, err := json.Marshal(jsonMarshallable) + if err != nil { + jww.FATAL.Panicf("Failed to JSON marshal %T for EventUpdate "+ + "callback: %+v", jsonMarshallable, err) + } + uiCallbacks.EventUpdate(eventType, data) + }, } - return wrapper, nil } @@ -149,15 +144,8 @@ func v1Upgrade(db *idb.Database) error { return err } - return nil -} - -// v1Upgrade performs the v1 -> v2 database upgrade. -// -// This can never be changed without permanently breaking backwards -// compatibility. -func v2Upgrade(db *idb.Database) error { - _, err := db.CreateObjectStore(fileStoreName, idb.ObjectStoreOptions{ + // Build File ObjectStore + _, err = db.CreateObjectStore(fileStoreName, idb.ObjectStoreOptions{ KeyPath: js.ValueOf(pkeyName), AutoIncrement: false, }) diff --git a/indexedDb/impl/channels/main.go b/indexedDb/impl/channels/main.go index 5290d0c89b6eedf92a1571aea04ff5b7fbfdc668..b84f13dc205ecd8bfe2466ea9ecb827770f31754 100644 --- a/indexedDb/impl/channels/main.go +++ b/indexedDb/impl/channels/main.go @@ -11,32 +11,70 @@ package main import ( "fmt" + "os" + "syscall/js" + + "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/logging" - "gitlab.com/elixxir/xxdk-wasm/wasm" "gitlab.com/elixxir/xxdk-wasm/worker" - "syscall/js" ) // SEMVER is the current semantic version of the xxDK channels web worker. const SEMVER = "0.1.0" -func init() { - // Set up Javascript console listener set at level INFO - ll := logging.NewJsConsoleLogListener(jww.LevelInfo) - logging.AddLogListener(ll.Listen) - jww.SetStdoutThreshold(jww.LevelFatal + 1) - jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER) +func main() { + // Set to os.Args because the default is os.Args[1:] and in WASM, args start + // at 0, not 1. + channelsCmd.SetArgs(os.Args) + + err := channelsCmd.Execute() + if err != nil { + fmt.Println(err) + os.Exit(1) + } } -func main() { - jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.") +var channelsCmd = &cobra.Command{ + Use: "channelsIndexedDbWorker", + Short: "IndexedDb database for channels.", + Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]", + Run: func(cmd *cobra.Command, args []string) { + // Start logger first to capture all logging events + err := logging.EnableLogging(logLevel, -1, 0, "", "") + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } - js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel)) + jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER) - m := &manager{mh: worker.NewThreadManager("ChannelsIndexedDbWorker", true)} - m.registerCallbacks() - m.mh.SignalReady() - <-make(chan bool) - fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.") + jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.") + m := &manager{ + wtm: worker.NewThreadManager("ChannelsIndexedDbWorker", true), + } + m.registerCallbacks() + m.wtm.SignalReady() + + // Indicate to the Javascript caller that the WASM is ready by resolving + // a promise created by the caller. + js.Global().Get("onWasmInitialized").Invoke() + + <-make(chan bool) + fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.") + os.Exit(0) + }, +} + +var ( + logLevel jww.Threshold +) + +func init() { + // Initialize all startup flags + channelsCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + "Sets the log level output when outputting to the Javascript console. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") } diff --git a/indexedDb/impl/dm/callbacks.go b/indexedDb/impl/dm/callbacks.go index 5fe03874609ddfb4c92a4680494ac6c475568ae3..380f04d9953fdda65c4b0d80a66e7b40a7edc2b1 100644 --- a/indexedDb/impl/dm/callbacks.go +++ b/indexedDb/impl/dm/callbacks.go @@ -29,24 +29,24 @@ var zeroUUID = []byte{0, 0, 0, 0, 0, 0, 0, 0} // manager handles the event model and the message callbacks, which is used to // send information between the event model and the main thread. type manager struct { - mh *worker.ThreadManager + wtm *worker.ThreadManager model dm.EventModel } // registerCallbacks registers all the reception callbacks to manage messages // from the main thread for the channels.EventModel. func (m *manager) registerCallbacks() { - m.mh.RegisterCallback(wDm.NewWASMEventModelTag, m.newWASMEventModelCB) - m.mh.RegisterCallback(wDm.ReceiveTag, m.receiveCB) - m.mh.RegisterCallback(wDm.ReceiveTextTag, m.receiveTextCB) - m.mh.RegisterCallback(wDm.ReceiveReplyTag, m.receiveReplyCB) - m.mh.RegisterCallback(wDm.ReceiveReactionTag, m.receiveReactionCB) - m.mh.RegisterCallback(wDm.UpdateSentStatusTag, m.updateSentStatusCB) - - m.mh.RegisterCallback(wDm.BlockSenderTag, m.blockSenderCB) - m.mh.RegisterCallback(wDm.UnblockSenderTag, m.unblockSenderCB) - m.mh.RegisterCallback(wDm.GetConversationTag, m.getConversationCB) - m.mh.RegisterCallback(wDm.GetConversationsTag, m.getConversationsCB) + m.wtm.RegisterCallback(wDm.NewWASMEventModelTag, m.newWASMEventModelCB) + m.wtm.RegisterCallback(wDm.ReceiveTag, m.receiveCB) + m.wtm.RegisterCallback(wDm.ReceiveTextTag, m.receiveTextCB) + m.wtm.RegisterCallback(wDm.ReceiveReplyTag, m.receiveReplyCB) + m.wtm.RegisterCallback(wDm.ReceiveReactionTag, m.receiveReactionCB) + m.wtm.RegisterCallback(wDm.UpdateSentStatusTag, m.updateSentStatusCB) + + m.wtm.RegisterCallback(wDm.BlockSenderTag, m.blockSenderCB) + m.wtm.RegisterCallback(wDm.UnblockSenderTag, m.unblockSenderCB) + m.wtm.RegisterCallback(wDm.GetConversationTag, m.getConversationCB) + m.wtm.RegisterCallback(wDm.GetConversationsTag, m.getConversationsCB) } // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty @@ -98,7 +98,7 @@ func (m *manager) messageReceivedCallback(uuid uint64, pubKey ed25519.PublicKey, } // Send it to the main thread - m.mh.SendMessage(wDm.MessageReceivedCallbackTag, data) + m.wtm.SendMessage(wDm.MessageReceivedCallbackTag, data) } // receiveCB is the callback for wasmModel.Receive. Returns a UUID of 0 on error diff --git a/indexedDb/impl/dm/dmIndexedDbWorker.js b/indexedDb/impl/dm/dmIndexedDbWorker.js index e199a7bb812b9ff119b7f130f41d3bb555247302..8a5fdbf8ad9a02967b408985a0219647003eaf7e 100644 --- a/indexedDb/impl/dm/dmIndexedDbWorker.js +++ b/indexedDb/impl/dm/dmIndexedDbWorker.js @@ -7,11 +7,15 @@ importScripts('wasm_exec.js'); +const isReady = new Promise((resolve) => { + self.onWasmInitialized = resolve; +}); + const go = new Go(); const binPath = 'xxdk-dmIndexedDkWorker.wasm' -WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => { +WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); - LogLevel(1); + await isReady; }).catch((err) => { console.error(err); }); \ No newline at end of file diff --git a/indexedDb/impl/dm/implementation.go b/indexedDb/impl/dm/implementation.go index 068b8d41450b3484b3fa22e05a4f8378b48c778a..a92691878ebc4b6c21cd1d38297aca23e4109920 100644 --- a/indexedDb/impl/dm/implementation.go +++ b/indexedDb/impl/dm/implementation.go @@ -13,6 +13,7 @@ import ( "bytes" "crypto/ed25519" "encoding/json" + "gitlab.com/xx_network/primitives/netTime" "strings" "syscall/js" "time" @@ -25,8 +26,8 @@ import ( "gitlab.com/elixxir/client/v4/dm" cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/message" + "gitlab.com/elixxir/wasm-utils/utils" "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" - "gitlab.com/elixxir/xxdk-wasm/utils" "gitlab.com/xx_network/primitives/id" ) @@ -42,16 +43,16 @@ type wasmModel struct { // upsertConversation is used for joining or updating a Conversation. func (w *wasmModel) upsertConversation(nickname string, pubKey ed25519.PublicKey, partnerToken uint32, codeset uint8, - blocked bool) error { + blockedTimestamp *time.Time) error { parentErr := errors.New("[DM indexedDB] failed to upsertConversation") // Build object newConvo := Conversation{ - Pubkey: pubKey, - Nickname: nickname, - Token: partnerToken, - CodesetVersion: codeset, - Blocked: blocked, + Pubkey: pubKey, + Nickname: nickname, + Token: partnerToken, + CodesetVersion: codeset, + BlockedTimestamp: blockedTimestamp, } // Convert to jsObject @@ -231,11 +232,11 @@ func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, n "[DM indexedDB] Joining conversation with %s", nickname) convoToUpdate = &Conversation{ - Pubkey: partnerKey, - Nickname: nickname, - Token: partnerToken, - CodesetVersion: codeset, - Blocked: false, + Pubkey: partnerKey, + Nickname: nickname, + Token: partnerToken, + CodesetVersion: codeset, + BlockedTimestamp: nil, } } } else { @@ -268,7 +269,7 @@ func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, n conversationUpdated := convoToUpdate != nil if conversationUpdated { err = w.upsertConversation(convoToUpdate.Nickname, convoToUpdate.Pubkey, - convoToUpdate.Token, convoToUpdate.CodesetVersion, convoToUpdate.Blocked) + convoToUpdate.Token, convoToUpdate.CodesetVersion, convoToUpdate.BlockedTimestamp) if err != nil { return 0, err } @@ -318,7 +319,8 @@ func (w *wasmModel) upsertMessage(msg *Message) (uint64, error) { // Store message to database msgIdObj, err := impl.Put(w.db, messageStoreName, messageObj) if err != nil { - return 0, errors.Errorf("Unable to put Message: %+v", err) + return 0, errors.Errorf("Unable to put Message: %+v\n%s", + err, newMessageJson) } uuid := msgIdObj.Int() @@ -348,14 +350,20 @@ func (w *wasmModel) UnblockSender(senderPubKey ed25519.PublicKey) { // setBlocked is a helper for blocking/unblocking a given Conversation. func (w *wasmModel) setBlocked(senderPubKey ed25519.PublicKey, isBlocked bool) error { - // Get current Conversation and set blocked + // Get current Conversation and set blocked accordingly resultConvo, err := w.getConversation(senderPubKey) if err != nil { return err } + var timeBlocked *time.Time + if isBlocked { + blockUser := netTime.Now() + timeBlocked = &blockUser + } + return w.upsertConversation(resultConvo.Nickname, resultConvo.Pubkey, - resultConvo.Token, resultConvo.CodesetVersion, isBlocked) + resultConvo.Token, resultConvo.CodesetVersion, timeBlocked) } // GetConversation returns the conversation held by the model (receiver). @@ -368,11 +376,11 @@ func (w *wasmModel) GetConversation(senderPubKey ed25519.PublicKey) *dm.ModelCon } return &dm.ModelConversation{ - Pubkey: resultConvo.Pubkey, - Nickname: resultConvo.Nickname, - Token: resultConvo.Token, - CodesetVersion: resultConvo.CodesetVersion, - Blocked: resultConvo.Blocked, + Pubkey: resultConvo.Pubkey, + Nickname: resultConvo.Nickname, + Token: resultConvo.Token, + CodesetVersion: resultConvo.CodesetVersion, + BlockedTimestamp: resultConvo.BlockedTimestamp, } } @@ -410,11 +418,11 @@ func (w *wasmModel) GetConversations() []dm.ModelConversation { return nil } conversations[i] = dm.ModelConversation{ - Pubkey: resultConvo.Pubkey, - Nickname: resultConvo.Nickname, - Token: resultConvo.Token, - CodesetVersion: resultConvo.CodesetVersion, - Blocked: resultConvo.Blocked, + Pubkey: resultConvo.Pubkey, + Nickname: resultConvo.Nickname, + Token: resultConvo.Token, + CodesetVersion: resultConvo.CodesetVersion, + BlockedTimestamp: resultConvo.BlockedTimestamp, } } return conversations diff --git a/indexedDb/impl/dm/implementation_test.go b/indexedDb/impl/dm/implementation_test.go index 8e05e6af5fff85af79ea0f15be9dc1e2f5a77c12..c8f42c3871bcfc841683e93b57e2ed4105ab000b 100644 --- a/indexedDb/impl/dm/implementation_test.go +++ b/indexedDb/impl/dm/implementation_test.go @@ -17,8 +17,8 @@ import ( "gitlab.com/elixxir/client/v4/cmix/rounds" "gitlab.com/elixxir/client/v4/dm" "gitlab.com/elixxir/crypto/message" + "gitlab.com/elixxir/wasm-utils/utils" "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" - "gitlab.com/elixxir/xxdk-wasm/utils" "gitlab.com/xx_network/primitives/id" "os" "syscall/js" @@ -102,7 +102,7 @@ func TestImpl_GetConversations(t *testing.T) { testBytes := []byte(fmt.Sprintf("%d", i)) testPubKey := ed25519.PublicKey(testBytes) err = m.upsertConversation("test", testPubKey, - uint32(i), uint8(i), false) + uint32(i), uint8(i), nil) if err != nil { t.Fatal(err.Error()) } @@ -133,28 +133,28 @@ func TestWasmModel_BlockSender(t *testing.T) { // Insert a test convo testPubKey := ed25519.PublicKey{} - err = m.upsertConversation("test", testPubKey, 0, 0, false) + err = m.upsertConversation("test", testPubKey, 0, 0, nil) if err != nil { t.Fatal(err.Error()) } // Default to unblocked result := m.GetConversation(testPubKey) - if result.Blocked { + if result.BlockedTimestamp != nil { t.Fatal("Expected blocked to be false") } // Now toggle blocked m.BlockSender(testPubKey) result = m.GetConversation(testPubKey) - if !result.Blocked { + if result.BlockedTimestamp == nil { t.Fatal("Expected blocked to be true") } // Now toggle blocked again m.UnblockSender(testPubKey) result = m.GetConversation(testPubKey) - if result.Blocked { + if result.BlockedTimestamp != nil { t.Fatal("Expected blocked to be false") } } diff --git a/indexedDb/impl/dm/init.go b/indexedDb/impl/dm/init.go index b9bad462303b8000c33713f08170f69ac64f39c2..8332866b95055e7d322308cdf8669d1be78a95ac 100644 --- a/indexedDb/impl/dm/init.go +++ b/indexedDb/impl/dm/init.go @@ -49,12 +49,13 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, 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) + jww.INFO.Printf("IndexDb version for %s is current: v%d", + databaseName, newVersion) return nil } - jww.INFO.Printf("IndexDb upgrade required: v%d -> v%d", - oldVersion, newVersion) + jww.INFO.Printf("IndexDb upgrade required for %s: v%d -> v%d", + databaseName, oldVersion, newVersion) if oldVersion == 0 && newVersion >= 1 { err := v1Upgrade(db) @@ -75,10 +76,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/dm/main.go b/indexedDb/impl/dm/main.go index 20b20a0856c78ac753798c9fd4a692e4a88e4852..96fae8e6fbdbce3767739891d4d7ea466e08149c 100644 --- a/indexedDb/impl/dm/main.go +++ b/indexedDb/impl/dm/main.go @@ -11,32 +11,71 @@ package main import ( "fmt" + "os" + "syscall/js" + + "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/logging" - "gitlab.com/elixxir/xxdk-wasm/wasm" "gitlab.com/elixxir/xxdk-wasm/worker" - "syscall/js" ) // SEMVER is the current semantic version of the xxDK DM web worker. const SEMVER = "0.1.0" -func init() { - // Set up Javascript console listener set at level INFO - ll := logging.NewJsConsoleLogListener(jww.LevelInfo) - logging.AddLogListener(ll.Listen) - jww.SetStdoutThreshold(jww.LevelFatal + 1) - jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER) +func main() { + // Set to os.Args because the default is os.Args[1:] and in WASM, args start + // at 0, not 1. + dmCmd.SetArgs(os.Args) + + err := dmCmd.Execute() + if err != nil { + fmt.Println(err) + os.Exit(1) + } } -func main() { - jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.") +var dmCmd = &cobra.Command{ + Use: "dmIndexedDbWorker", + Short: "IndexedDb database for DMs.", + Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]", + Run: func(cmd *cobra.Command, args []string) { + // Start logger first to capture all logging events + err := logging.EnableLogging(logLevel, -1, 0, "", "") + if err != nil { + fmt.Printf( + "Failed to intialize logging in DM indexedDb worker: %+v", err) + os.Exit(1) + } - js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel)) + jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER) - m := &manager{mh: worker.NewThreadManager("DmIndexedDbWorker", true)} - m.registerCallbacks() - m.mh.SignalReady() - <-make(chan bool) - fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.") + jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.") + m := &manager{ + wtm: worker.NewThreadManager("DmIndexedDbWorker", true), + } + m.registerCallbacks() + m.wtm.SignalReady() + + // Indicate to the Javascript caller that the WASM is ready by resolving + // a promise created by the caller. + js.Global().Get("onWasmInitialized").Invoke() + + <-make(chan bool) + fmt.Println("[WW] Closing xxDK WebAssembly Channels Database Worker.") + os.Exit(0) + }, +} + +var ( + logLevel jww.Threshold +) + +func init() { + // Initialize all startup flags + dmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + "Sets the log level output when outputting to the Javascript console. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") } diff --git a/indexedDb/impl/dm/model.go b/indexedDb/impl/dm/model.go index dd4fee16205c9736936830bbb598252dc0774e05..774d011fe4987078808febd74a1657839e06dc8a 100644 --- a/indexedDb/impl/dm/model.go +++ b/indexedDb/impl/dm/model.go @@ -55,9 +55,9 @@ type Message struct { // 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"` + Pubkey []byte `json:"pub_key"` // Matches convoPkeyName + Nickname string `json:"nickname"` + Token uint32 `json:"token"` + CodesetVersion uint8 `json:"codeset_version"` + BlockedTimestamp *time.Time `json:"blocked_timestamp"` } diff --git a/indexedDb/impl/state/callbacks.go b/indexedDb/impl/state/callbacks.go new file mode 100644 index 0000000000000000000000000000000000000000..fbeb674ad08e8ab9c6f0ef95fff05e96d178a7d5 --- /dev/null +++ b/indexedDb/impl/state/callbacks.go @@ -0,0 +1,79 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 main + +import ( + "encoding/json" + "github.com/pkg/errors" + + "gitlab.com/elixxir/client/v4/storage/utility" + stateWorker "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/state" + "gitlab.com/elixxir/xxdk-wasm/worker" +) + +// manager handles the message callbacks, which is used to +// send information between the model and the main thread. +type manager struct { + wtm *worker.ThreadManager + model utility.WebState +} + +// registerCallbacks registers all the reception callbacks to manage messages +// from the main thread. +func (m *manager) registerCallbacks() { + m.wtm.RegisterCallback(stateWorker.NewStateTag, m.newStateCB) + m.wtm.RegisterCallback(stateWorker.SetTag, m.setCB) + m.wtm.RegisterCallback(stateWorker.GetTag, m.getCB) +} + +// newStateCB is the callback for NewState. Returns an empty +// slice on success or an error message on failure. +func (m *manager) newStateCB(data []byte) ([]byte, error) { + var msg stateWorker.NewStateMessage + err := json.Unmarshal(data, &msg) + if err != nil { + return []byte{}, errors.Errorf( + "failed to JSON unmarshal %T from main thread: %+v", msg, err) + } + + m.model, err = NewState(msg.DatabaseName) + if err != nil { + return []byte(err.Error()), nil + } + + return []byte{}, nil +} + +// setCB is the callback for stateModel.Set. +// Returns nil on error or the resulting byte data on success. +func (m *manager) setCB(data []byte) ([]byte, error) { + var msg stateWorker.TransferMessage + err := json.Unmarshal(data, &msg) + if err != nil { + return nil, errors.Errorf( + "failed to JSON unmarshal %T from main thread: %+v", msg, err) + } + + return nil, m.model.Set(msg.Key, msg.Value) +} + +// getCB is the callback for stateModel.Get. +// Returns nil on error or the resulting byte data on success. +func (m *manager) getCB(data []byte) ([]byte, error) { + key := string(data) + result, err := m.model.Get(key) + msg := stateWorker.TransferMessage{ + Key: key, + Value: result, + Error: err.Error(), + } + + return json.Marshal(msg) +} diff --git a/indexedDb/impl/state/implementation.go b/indexedDb/impl/state/implementation.go new file mode 100644 index 0000000000000000000000000000000000000000..f580eff52c86978782226d9a5adb305d33d0013c --- /dev/null +++ b/indexedDb/impl/state/implementation.go @@ -0,0 +1,66 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 main + +import ( + "encoding/json" + "github.com/hack-pad/go-indexeddb/idb" + "github.com/pkg/errors" + "gitlab.com/elixxir/wasm-utils/utils" + "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" + "syscall/js" +) + +// stateModel implements [ClientState] interface backed by IndexedDb. +// NOTE: This model is NOT thread safe - it is the responsibility of the +// caller to ensure that its methods are called sequentially. +type stateModel struct { + db *idb.Database +} + +func (s *stateModel) Get(key string) ([]byte, error) { + result, err := impl.Get(s.db, stateStoreName, js.ValueOf(key)) + if err != nil { + return nil, err + } + + stateObj := &State{} + err = json.Unmarshal([]byte(utils.JsToJson(result)), stateObj) + if err != nil { + return nil, err + } + + return stateObj.Value, err +} + +func (s *stateModel) Set(key string, value []byte) error { + state := &State{ + Id: key, + Value: value, + } + + // Convert to jsObject + newStateJSON, err := json.Marshal(state) + if err != nil { + return errors.Errorf("Unable to marshal State: %+v", err) + } + stateObj, err := utils.JsonToJS(newStateJSON) + if err != nil { + return errors.Errorf("Unable to marshal State: %+v", err) + } + + // Store State to database + _, err = impl.Put(s.db, stateStoreName, stateObj) + if err != nil { + return errors.Errorf("Unable to put State: %+v\n%s", + err, newStateJSON) + } + return nil +} diff --git a/indexedDb/impl/state/init.go b/indexedDb/impl/state/init.go new file mode 100644 index 0000000000000000000000000000000000000000..e5ba9f9add4bd0f55c092a8ddd40ce97d936ebf0 --- /dev/null +++ b/indexedDb/impl/state/init.go @@ -0,0 +1,84 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 main + +import ( + "github.com/hack-pad/go-indexeddb/idb" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/storage/utility" + "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" + "syscall/js" +) + +// currentVersion is the current version of the IndexedDb runtime. Used for +// migration purposes. +const currentVersion uint = 1 + +// NewState returns a [utility.WebState] backed by IndexedDb. +// The name should be a base64 encoding of the users public key. +func NewState(databaseName string) (utility.WebState, error) { + return newState(databaseName) +} + +// newState creates the given [idb.Database] and returns a stateModel. +func newState(databaseName string) (*stateModel, error) { + // Attempt to open database object + ctx, cancel := impl.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 for %s is current: v%d", + databaseName, newVersion) + return nil + } + + jww.INFO.Printf("IndexDb upgrade required for %s: v%d -> v%d", + databaseName, 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 + } else if ctx.Err() != nil { + return nil, ctx.Err() + } + + wrapper := &stateModel{db: db} + 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 { + storeOpts := idb.ObjectStoreOptions{ + KeyPath: js.ValueOf(pkeyName), + AutoIncrement: false, + } + _, err := db.CreateObjectStore(stateStoreName, storeOpts) + return err +} diff --git a/indexedDb/impl/state/main.go b/indexedDb/impl/state/main.go new file mode 100644 index 0000000000000000000000000000000000000000..719b9969852c2ba946d6365367050878b0dd7b81 --- /dev/null +++ b/indexedDb/impl/state/main.go @@ -0,0 +1,80 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 main + +import ( + "fmt" + "os" + "syscall/js" + + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/xxdk-wasm/logging" + "gitlab.com/elixxir/xxdk-wasm/worker" +) + +// SEMVER is the current semantic version of the xxDK web worker. +const SEMVER = "0.1.0" + +func main() { + // Set to os.Args because the default is os.Args[1:] and in WASM, args start + // at 0, not 1. + channelsCmd.SetArgs(os.Args) + + err := channelsCmd.Execute() + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +var channelsCmd = &cobra.Command{ + Use: "stateIndexedDbWorker", + Short: "IndexedDb database for state.", + Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]", + Run: func(cmd *cobra.Command, args []string) { + // Start logger first to capture all logging events + err := logging.EnableLogging(logLevel, -1, 0, "", "") + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } + + jww.INFO.Printf("xxDK state web worker version: v%s", SEMVER) + + jww.INFO.Print("[WW] Starting xxDK WebAssembly State Database Worker.") + m := &manager{ + wtm: worker.NewThreadManager("StateIndexedDbWorker", true), + } + m.registerCallbacks() + m.wtm.SignalReady() + + // Indicate to the Javascript caller that the WASM is ready by resolving + // a promise created by the caller. + js.Global().Get("onWasmInitialized").Invoke() + + <-make(chan bool) + fmt.Println("[WW] Closing xxDK WebAssembly State Database Worker.") + os.Exit(0) + }, +} + +var ( + logLevel jww.Threshold +) + +func init() { + // Initialize all startup flags + channelsCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + "Sets the log level output when outputting to the Javascript console. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") +} diff --git a/indexedDb/impl/state/model.go b/indexedDb/impl/state/model.go new file mode 100644 index 0000000000000000000000000000000000000000..5b8638a5ff832cdc028d5dfb7a103faeb9118749 --- /dev/null +++ b/indexedDb/impl/state/model.go @@ -0,0 +1,27 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 main + +const ( + // Text representation of primary key value (keyPath). + pkeyName = "id" + + // Text representation of the names of the various [idb.ObjectStore]. + stateStoreName = "states" +) + +// State defines the IndexedDb representation of a single KV data store. +type State struct { + // Id is a unique identifier for a given State. + Id string `json:"id"` // Matches pkeyName + + // Value stores the data contents of the State. + Value []byte `json:"value"` +} diff --git a/indexedDb/impl/state/stateIndexedDbWorker.js b/indexedDb/impl/state/stateIndexedDbWorker.js new file mode 100644 index 0000000000000000000000000000000000000000..a7c440d2a862a81737d75418ef39e5464c99ad90 --- /dev/null +++ b/indexedDb/impl/state/stateIndexedDbWorker.js @@ -0,0 +1,21 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +importScripts('wasm_exec.js'); + +const isReady = new Promise((resolve) => { + self.onWasmInitialized = resolve; +}); + +const go = new Go(); +const binPath = 'xxdk-stateIndexedDkWorker.wasm' +WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { + go.run(result.instance); + await isReady; +}).catch((err) => { + console.error(err); +}); \ No newline at end of file diff --git a/indexedDb/impl/utils.go b/indexedDb/impl/utils.go index 3765cfce5e01698ca625844b5fe495906985366a..7dbf631c9deee97f3ef79b731832aab142820fbc 100644 --- a/indexedDb/impl/utils.go +++ b/indexedDb/impl/utils.go @@ -18,7 +18,7 @@ import ( "github.com/hack-pad/go-indexeddb/idb" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" "time" ) @@ -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,17 +84,15 @@ 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) @@ -103,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 { @@ -119,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()) } @@ -150,17 +170,15 @@ 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) @@ -189,23 +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)) } 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 @@ -226,16 +242,14 @@ 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) @@ -282,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 { @@ -303,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)) +} diff --git a/indexedDb/worker/channels/implementation.go b/indexedDb/worker/channels/implementation.go index 9639bc185095e74cbf4b3e63256fead39bbcef82..2e11702432dd5d4b1dae668715ccb6a7bdc50950 100644 --- a/indexedDb/worker/channels/implementation.go +++ b/indexedDb/worker/channels/implementation.go @@ -452,16 +452,16 @@ func (w *wasmModel) DeleteMessage(messageID message.ID) error { // MuteUserMessage is JSON marshalled and sent to the worker for // [wasmModel.MuteUser]. type MuteUserMessage struct { - ChannelID *id.ID `json:"channelID"` - PubKey ed25519.PublicKey `json:"pubKey"` - Unmute bool `json:"unmute"` + ChannelID []byte `json:"channelID"` + PubKey []byte `json:"pubKey"` + Unmute bool `json:"unmute"` } // MuteUser is called whenever a user is muted or unmuted. func (w *wasmModel) MuteUser( channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) { msg := MuteUserMessage{ - ChannelID: channelID, + ChannelID: channelID.Marshal(), PubKey: pubKey, Unmute: unmute, } diff --git a/indexedDb/worker/channels/init.go b/indexedDb/worker/channels/init.go index 2ee630caf43177460a19f14d2b953d3a03b9c687..d24120aa445d6c161bf148a242439f4a6bf05eb1 100644 --- a/indexedDb/worker/channels/init.go +++ b/indexedDb/worker/channels/init.go @@ -10,47 +10,37 @@ package channels import ( - "crypto/ed25519" "encoding/json" - "github.com/pkg/errors" "time" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/client/v4/channels" cryptoChannel "gitlab.com/elixxir/crypto/channel" - "gitlab.com/elixxir/crypto/message" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/elixxir/xxdk-wasm/worker" - "gitlab.com/xx_network/primitives/id" ) // databaseSuffix is the suffix to be appended to the name of the database. const databaseSuffix = "_speakeasy" -// MessageReceivedCallback is called any time a message is received or updated. -// -// update is true if the row is old and was edited. -type MessageReceivedCallback func(uuid uint64, channelID *id.ID, update bool) - -// DeletedMessageCallback is called any time a message is deleted. -type DeletedMessageCallback func(messageID message.ID) - -// MutedUserCallback is called any time a user is muted or unmuted. unmute is -// true if the user has been unmuted and false if they have been muted. -type MutedUserCallback func( - channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) +// eventUpdateCallback is the [bindings.ChannelUICallback] callback function +// it has a type ([bindings.NickNameUpdate] to [bindings.MessageDeleted] +// and json data that is the callback information. +type eventUpdateCallback func(eventType int64, jsonData []byte) // NewWASMEventModelBuilder returns an EventModelBuilder which allows // the channel manager to define the path but the callback is the same // across the board. func NewWASMEventModelBuilder(wasmJsPath string, - encryption cryptoChannel.Cipher, messageReceivedCB MessageReceivedCallback, - deletedMessageCB DeletedMessageCallback, - mutedUserCB MutedUserCallback) channels.EventModelBuilder { + encryption cryptoChannel.Cipher, + channelCbs bindings.ChannelUICallbacks) channels.EventModelBuilder { fn := func(path string) (channels.EventModel, error) { return NewWASMEventModel(path, wasmJsPath, encryption, - messageReceivedCB, deletedMessageCB, mutedUserCB) + channelCbs) } return fn } @@ -65,8 +55,7 @@ type NewWASMEventModelMessage struct { // NewWASMEventModel returns a [channels.EventModel] backed by a wasmModel. // The name should be a base64 encoding of the users public key. func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, - messageReceivedCB MessageReceivedCallback, - deletedMessageCB DeletedMessageCallback, mutedUserCB MutedUserCallback) ( + channelCbs bindings.ChannelUICallbacks) ( channels.EventModel, error) { databaseName := path + databaseSuffix @@ -75,17 +64,9 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, return nil, err } - // Register handler to manage messages for the MessageReceivedCallback - wm.RegisterCallback(MessageReceivedCallbackTag, - messageReceivedCallbackHandler(messageReceivedCB)) - - // Register handler to manage messages for the DeletedMessageCallback - wm.RegisterCallback(DeletedMessageCallbackTag, - deletedMessageCallbackHandler(deletedMessageCB)) - - // Register handler to manage messages for the MutedUserCallback - wm.RegisterCallback(MutedUserCallbackTag, - mutedUserCallbackHandler(mutedUserCB)) + // Register handler to manage messages for the EventUpdate + wm.RegisterCallback(EventUpdateCallbackTag, + messageReceivedCallbackHandler(channelCbs.EventUpdate)) // Store the database name err = storage.StoreIndexedDb(databaseName) @@ -132,49 +113,18 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, return &wasmModel{wm}, nil } -// MessageReceivedCallbackMessage is JSON marshalled and received from the -// worker for the [MessageReceivedCallback] callback. -type MessageReceivedCallbackMessage struct { - UUID uint64 `json:"uuid"` - ChannelID *id.ID `json:"channelID"` - Update bool `json:"update"` +// EventUpdateCallbackMessage is JSON marshalled and received from the worker +// for the [EventUpdate] callback. +type EventUpdateCallbackMessage struct { + EventType int64 `json:"eventType"` + JsonData []byte `json:"jsonData"` } // messageReceivedCallbackHandler returns a handler to manage messages for the // MessageReceivedCallback. -func messageReceivedCallbackHandler(cb MessageReceivedCallback) func(data []byte) { - return func(data []byte) { - var msg MessageReceivedCallbackMessage - err := json.Unmarshal(data, &msg) - if err != nil { - jww.ERROR.Printf( - "Failed to JSON unmarshal %T from worker: %+v", msg, err) - return - } - - cb(msg.UUID, msg.ChannelID, msg.Update) - } -} - -// deletedMessageCallbackHandler returns a handler to manage messages for the -// DeletedMessageCallback. -func deletedMessageCallbackHandler(cb DeletedMessageCallback) func(data []byte) { - return func(data []byte) { - messageID, err := message.UnmarshalID(data) - if err != nil { - jww.ERROR.Printf( - "Failed to JSON unmarshal message ID from worker: %+v", err) - } - - cb(messageID) - } -} - -// mutedUserCallbackHandler returns a handler to manage messages for the -// MutedUserCallback. -func mutedUserCallbackHandler(cb MutedUserCallback) func(data []byte) { +func messageReceivedCallbackHandler(cb eventUpdateCallback) func(data []byte) { return func(data []byte) { - var msg MuteUserMessage + var msg EventUpdateCallbackMessage err := json.Unmarshal(data, &msg) if err != nil { jww.ERROR.Printf( @@ -182,7 +132,7 @@ func mutedUserCallbackHandler(cb MutedUserCallback) func(data []byte) { return } - cb(msg.ChannelID, msg.PubKey, msg.Unmute) + cb(msg.EventType, msg.JsonData) } } diff --git a/indexedDb/worker/channels/tags.go b/indexedDb/worker/channels/tags.go index d3555e549163c18b772cc86f965a2f7aeca7a827..f21c91653547cfe37158d514ef482c468084d800 100644 --- a/indexedDb/worker/channels/tags.go +++ b/indexedDb/worker/channels/tags.go @@ -14,10 +14,8 @@ import "gitlab.com/elixxir/xxdk-wasm/worker" // List of tags that can be used when sending a message or registering a handler // to receive a message. const ( - NewWASMEventModelTag worker.Tag = "NewWASMEventModel" - MessageReceivedCallbackTag worker.Tag = "MessageReceivedCallback" - DeletedMessageCallbackTag worker.Tag = "DeletedMessageCallback" - MutedUserCallbackTag worker.Tag = "MutedUserCallback" + NewWASMEventModelTag worker.Tag = "NewWASMEventModel" + EventUpdateCallbackTag worker.Tag = "EventUpdateCallback" JoinChannelTag worker.Tag = "JoinChannel" LeaveChannelTag worker.Tag = "LeaveChannel" diff --git a/indexedDb/worker/state/implementation.go b/indexedDb/worker/state/implementation.go new file mode 100644 index 0000000000000000000000000000000000000000..92a5752ade4aef34f002a3904cc586f047e4c6bb --- /dev/null +++ b/indexedDb/worker/state/implementation.go @@ -0,0 +1,82 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 dm + +import ( + "encoding/json" + "github.com/pkg/errors" + "time" + + "gitlab.com/elixxir/xxdk-wasm/worker" +) + +type wasmModel struct { + wh *worker.Manager +} + +// TransferMessage is JSON marshalled and sent to the worker. +type TransferMessage struct { + Key string `json:"key"` + Value []byte `json:"value"` + Error string `json:"error"` +} + +func (w *wasmModel) Set(key string, value []byte) error { + msg := TransferMessage{ + Key: key, + Value: value, + } + + data, err := json.Marshal(msg) + if err != nil { + return errors.Errorf( + "Could not JSON marshal payload for TransferMessage: %+v", err) + } + + resultChan := make(chan []byte) + w.wh.SendMessage(SetTag, data, + func(data []byte) { + resultChan <- data + }) + + select { + case result := <-resultChan: + return errors.New(string(result)) + case <-time.After(worker.ResponseTimeout): + return errors.Errorf("Timed out after %s waiting for response from the "+ + "worker about Get", worker.ResponseTimeout) + } +} + +func (w *wasmModel) Get(key string) ([]byte, error) { + resultChan := make(chan []byte) + w.wh.SendMessage(GetTag, []byte(key), + func(data []byte) { + resultChan <- data + }) + + select { + case result := <-resultChan: + var msg TransferMessage + err := json.Unmarshal(result, &msg) + if err != nil { + return nil, errors.Errorf( + "failed to JSON unmarshal %T from main thread: %+v", msg, err) + } + + if len(msg.Error) > 0 { + return nil, errors.New(msg.Error) + } + return msg.Value, nil + case <-time.After(worker.ResponseTimeout): + return nil, errors.Errorf("Timed out after %s waiting for response from the "+ + "worker about Get", worker.ResponseTimeout) + } +} diff --git a/indexedDb/worker/state/init.go b/indexedDb/worker/state/init.go new file mode 100644 index 0000000000000000000000000000000000000000..b4842ae5ca954e1bfead70a483690b88fbf1974d --- /dev/null +++ b/indexedDb/worker/state/init.go @@ -0,0 +1,72 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 dm + +import ( + "encoding/json" + "time" + + "github.com/pkg/errors" + + "gitlab.com/elixxir/client/v4/storage/utility" + "gitlab.com/elixxir/xxdk-wasm/storage" + "gitlab.com/elixxir/xxdk-wasm/worker" +) + +// databaseSuffix is the suffix to be appended to the name of the database. +const databaseSuffix = "_speakeasy_state" + +// NewStateMessage is JSON marshalled and sent to the worker for +// [NewState]. +type NewStateMessage struct { + DatabaseName string `json:"databaseName"` +} + +// NewState returns a [utility.WebState] backed by indexeddb. +// The name should be a base64 encoding of the users public key. +func NewState(path, wasmJsPath string) (utility.WebState, error) { + databaseName := path + databaseSuffix + + wh, err := worker.NewManager(wasmJsPath, "stateIndexedDb", true) + if err != nil { + return nil, err + } + + // Store the database name + err = storage.StoreIndexedDb(databaseName) + if err != nil { + return nil, err + } + + msg := NewStateMessage{ + DatabaseName: databaseName, + } + + payload, err := json.Marshal(msg) + if err != nil { + return nil, err + } + + dataChan := make(chan []byte) + wh.SendMessage(NewStateTag, payload, + func(data []byte) { dataChan <- data }) + + select { + case data := <-dataChan: + if len(data) > 0 { + return nil, errors.New(string(data)) + } + case <-time.After(worker.ResponseTimeout): + return nil, errors.Errorf("timed out after %s waiting for indexedDB "+ + "database in worker to initialize", worker.ResponseTimeout) + } + + return &wasmModel{wh}, nil +} diff --git a/indexedDb/worker/state/tags.go b/indexedDb/worker/state/tags.go new file mode 100644 index 0000000000000000000000000000000000000000..0f6327d9f54509735fd6e6a1d7de57e3b8015a04 --- /dev/null +++ b/indexedDb/worker/state/tags.go @@ -0,0 +1,20 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 dm + +import "gitlab.com/elixxir/xxdk-wasm/worker" + +// List of tags that can be used when sending a message or registering a handler +// to receive a message. +const ( + NewStateTag worker.Tag = "NewState" + SetTag worker.Tag = "Set" + GetTag worker.Tag = "Get" +) diff --git a/logging/fileLogger.go b/logging/fileLogger.go new file mode 100644 index 0000000000000000000000000000000000000000..831511a81e069b1899b081d40268cf38042cf0b7 --- /dev/null +++ b/logging/fileLogger.go @@ -0,0 +1,95 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 logging + +import ( + "io" + "math" + + "github.com/armon/circbuf" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/xxdk-wasm/worker" +) + +// fileLogger manages the recording of jwalterweatherman logs to the local +// in-memory file buffer. +type fileLogger struct { + threshold jww.Threshold + cb *circbuf.Buffer +} + +// newFileLogger starts logging to a local, in-memory log file at the specified +// threshold. Returns a [fileLogger] that can be used to get the log file. +func newFileLogger(threshold jww.Threshold, maxLogFileSize int) (*fileLogger, error) { + b, err := circbuf.NewBuffer(int64(maxLogFileSize)) + if err != nil { + return nil, errors.Wrap(err, "could not create new circular buffer") + } + + fl := &fileLogger{ + threshold: threshold, + cb: b, + } + + jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level %s", + b.Size(), fl.threshold) + + logger = fl + return fl, nil +} + +// Write adheres to the io.Writer interface and writes log entries to the +// buffer. +func (fl *fileLogger) Write(p []byte) (n int, err error) { + return fl.cb.Write(p) +} + +// Listen adheres to the [jwalterweatherman.LogListener] type and returns the +// log writer when the threshold is within the set threshold limit. +func (fl *fileLogger) Listen(threshold jww.Threshold) io.Writer { + if threshold < fl.threshold { + return nil + } + return fl +} + +// StopLogging stops log message writes. Once logging is stopped, it cannot be +// resumed and the log file cannot be recovered. +func (fl *fileLogger) StopLogging() { + fl.threshold = math.MaxInt + fl.cb.Reset() +} + +// GetFile returns the entire log file. +func (fl *fileLogger) GetFile() []byte { + return fl.cb.Bytes() +} + +// Threshold returns the log level threshold used in the file. +func (fl *fileLogger) Threshold() jww.Threshold { + return fl.threshold +} + +// MaxSize returns the max size, in bytes, that the log file is allowed to be. +func (fl *fileLogger) MaxSize() int { + return int(fl.cb.Size()) +} + +// Size returns the current size, in bytes, written to the log file. +func (fl *fileLogger) Size() int { + return int(fl.cb.TotalWritten()) +} + +// Worker returns nil. +func (fl *fileLogger) Worker() *worker.Manager { + return nil +} diff --git a/logging/fileLogger_test.go b/logging/fileLogger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..317f69544a927280827aa4f3739c7f1e39d30ac4 --- /dev/null +++ b/logging/fileLogger_test.go @@ -0,0 +1,228 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 logging + +import ( + "bytes" + "github.com/armon/circbuf" + jww "github.com/spf13/jwalterweatherman" + "math/rand" + "reflect" + "testing" +) + +func Test_newFileLogger(t *testing.T) { + expected := &fileLogger{ + threshold: jww.LevelError, + } + expected.cb, _ = circbuf.NewBuffer(512) + fl, err := newFileLogger(expected.threshold, int(expected.cb.Size())) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + if !reflect.DeepEqual(expected, fl) { + t.Errorf("Unexpected new fileLogger.\nexpected: %+v\nreceived: %+v", + expected, fl) + } + if !reflect.DeepEqual(logger, fl) { + t.Errorf("Failed to set logger.\nexpected: %+v\nreceived: %+v", + logger, fl) + } + +} + +// Tests that fileLogger.Write writes the expected data to the buffer and that +// when the max file size is reached, old data is replaced. +func Test_fileLogger_Write(t *testing.T) { + rng := rand.New(rand.NewSource(3424)) + fl, err := newFileLogger(jww.LevelError, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + expected := make([]byte, fl.MaxSize()) + rng.Read(expected) + n, err := fl.Write(expected) + if err != nil { + t.Fatalf("Failed to write: %+v", err) + } else if n != len(expected) { + t.Fatalf("Did not write expected length.\nexpected: %d\nreceived: %d", + len(expected), n) + } + + if !bytes.Equal(fl.cb.Bytes(), expected) { + t.Fatalf("Incorrect bytes in buffer.\nexpected: %v\nreceived: %v", + expected, fl.cb.Bytes()) + } + + // Check that the data is overwritten + rng.Read(expected) + n, err = fl.Write(expected) + if err != nil { + t.Fatalf("Failed to write: %+v", err) + } else if n != len(expected) { + t.Fatalf("Did not write expected length.\nexpected: %d\nreceived: %d", + len(expected), n) + } + + if !bytes.Equal(fl.cb.Bytes(), expected) { + t.Fatalf("Incorrect bytes in buffer.\nexpected: %v\nreceived: %v", + expected, fl.cb.Bytes()) + } +} + +// Tests that fileLogger.Listen only returns an io.Writer for valid thresholds. +func Test_fileLogger_Listen(t *testing.T) { + th := jww.LevelError + fl, err := newFileLogger(th, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + thresholds := []jww.Threshold{-1, jww.LevelTrace, jww.LevelDebug, + jww.LevelFatal, jww.LevelWarn, jww.LevelError, jww.LevelCritical, + jww.LevelFatal} + + for _, threshold := range thresholds { + w := fl.Listen(threshold) + if threshold < th { + if w != nil { + t.Errorf("Did not receive nil io.Writer for level %s: %+v", + threshold, w) + } + } else if w == nil { + t.Errorf("Received nil io.Writer for level %s", threshold) + } + } +} + +// Tests that fileLogger.Listen always returns nil after fileLogger.StopLogging +// is called. +func Test_fileLogger_StopLogging(t *testing.T) { + fl, err := newFileLogger(jww.LevelError, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + fl.StopLogging() + + if w := fl.Listen(jww.LevelFatal); w != nil { + t.Errorf("Listen returned non-nil io.Writer when logging should have "+ + "been stopped: %+v", w) + } + + file := fl.GetFile() + if !bytes.Equal([]byte{}, file) { + t.Errorf("Did not receice empty file: %+v", file) + } +} + +// Tests that fileLogger.GetFile returns the expected file. +func Test_fileLogger_GetFile(t *testing.T) { + rng := rand.New(rand.NewSource(9863)) + fl, err := newFileLogger(jww.LevelError, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + var expected []byte + for i := 0; i < 5; i++ { + p := make([]byte, rng.Intn(64)) + rng.Read(p) + expected = append(expected, p...) + + if _, err = fl.Write(p); err != nil { + t.Errorf("Write %d failed: %+v", i, err) + } + } + + file := fl.GetFile() + if !bytes.Equal(expected, file) { + t.Errorf("Unexpected file.\nexpected: %v\nreceived: %v", expected, file) + } +} + +// Tests that fileLogger.Threshold returns the expected threshold. +func Test_fileLogger_Threshold(t *testing.T) { + thresholds := []jww.Threshold{-1, jww.LevelTrace, jww.LevelDebug, + jww.LevelFatal, jww.LevelWarn, jww.LevelError, jww.LevelCritical, + jww.LevelFatal} + + for _, threshold := range thresholds { + fl, err := newFileLogger(threshold, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + if fl.Threshold() != threshold { + t.Errorf("Incorrect threshold.\nexpected: %s (%d)\nreceived: %s (%d)", + threshold, threshold, fl.Threshold(), fl.Threshold()) + } + } +} + +// Unit test of fileLogger.MaxSize. +func Test_fileLogger_MaxSize(t *testing.T) { + maxSize := 512 + fl, err := newFileLogger(jww.LevelError, maxSize) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + if fl.MaxSize() != maxSize { + t.Errorf("Incorrect max size.\nexpected: %d\nreceived: %d", + maxSize, fl.MaxSize()) + } +} + +// Unit test of fileLogger.Size. +func Test_fileLogger_Size(t *testing.T) { + rng := rand.New(rand.NewSource(9863)) + fl, err := newFileLogger(jww.LevelError, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + var expected []byte + for i := 0; i < 5; i++ { + p := make([]byte, rng.Intn(64)) + rng.Read(p) + expected = append(expected, p...) + + if _, err = fl.Write(p); err != nil { + t.Errorf("Write %d failed: %+v", i, err) + } + + size := fl.Size() + if size != len(expected) { + t.Errorf("Incorrect size (%d).\nexpected: %d\nreceived: %d", + i, len(expected), size) + } + } + + file := fl.GetFile() + if !bytes.Equal(expected, file) { + t.Errorf("Unexpected file.\nexpected: %v\nreceived: %v", expected, file) + } +} + +// Tests that fileLogger.Worker always returns nil. +func Test_fileLogger_Worker(t *testing.T) { + fl, err := newFileLogger(jww.LevelError, 512) + if err != nil { + t.Fatalf("Failed to make new fileLogger: %+v", err) + } + + w := fl.Worker() + if w != nil { + t.Errorf("Did not get nil worker: %+v", w) + } +} diff --git a/logging/logLevel.go b/logging/logLevel.go deleted file mode 100644 index 895857475c4c647d7625b43644e6c4f32be98155..0000000000000000000000000000000000000000 --- a/logging/logLevel.go +++ /dev/null @@ -1,89 +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 logging - -import ( - "fmt" - "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" - "log" - "syscall/js" -) - -// LogLevel sets level of logging. All logs at the set level and below will be -// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL -// messages will be printed). -// -// The default log level without updates is INFO. -func LogLevel(threshold jww.Threshold) error { - if threshold < jww.LevelTrace || threshold > jww.LevelFatal { - return errors.Errorf("log level is not valid: log level: %d", threshold) - } - - jww.SetLogThreshold(threshold) - jww.SetFlags(log.LstdFlags | log.Lmicroseconds) - - ll := NewJsConsoleLogListener(threshold) - AddLogListener(ll.Listen) - jww.SetStdoutThreshold(jww.LevelFatal + 1) - - msg := fmt.Sprintf("Log level set to: %s", threshold) - switch threshold { - case jww.LevelTrace: - fallthrough - case jww.LevelDebug: - fallthrough - case jww.LevelInfo: - jww.INFO.Print(msg) - case jww.LevelWarn: - jww.WARN.Print(msg) - case jww.LevelError: - jww.ERROR.Print(msg) - case jww.LevelCritical: - jww.CRITICAL.Print(msg) - case jww.LevelFatal: - jww.FATAL.Print(msg) - } - - return nil -} - -// LogLevelJS sets level of logging. All logs at the set level and below will be -// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL -// messages will be printed). -// -// Log level options: -// -// TRACE - 0 -// DEBUG - 1 -// INFO - 2 -// WARN - 3 -// ERROR - 4 -// CRITICAL - 5 -// FATAL - 6 -// -// The default log level without updates is INFO. -// -// Parameters: -// - args[0] - Log level (int). -// -// Returns: -// - Throws TypeError if the log level is invalid. -func LogLevelJS(_ js.Value, args []js.Value) any { - threshold := jww.Threshold(args[0].Int()) - err := LogLevel(threshold) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil - } - - return nil -} diff --git a/logging/logger.go b/logging/logger.go index 03bd1cf4abebde1e4645bc87d31089ed9bf1c5d3..8155210daf6b0b81667334d8675f63bd6b2aaca5 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -10,29 +10,13 @@ package logging import ( - "encoding/binary" - "encoding/json" - "fmt" - "github.com/armon/circbuf" "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" - "gitlab.com/elixxir/xxdk-wasm/worker" - "io" - "strconv" - "sync/atomic" "syscall/js" - "time" -) -const ( - // DefaultInitThreshold is the log threshold used for the initial log before - // any logging options is set. - DefaultInitThreshold = jww.LevelTrace + jww "github.com/spf13/jwalterweatherman" - // logListenerChanSize is the size of the listener channel that stores log - // messages before they are written. - logListenerChanSize = 3000 + "gitlab.com/elixxir/wasm-utils/utils" + "gitlab.com/elixxir/xxdk-wasm/worker" ) // List of tags that can be used when sending a message or registering a handler @@ -46,346 +30,79 @@ const ( ) // logger is the global that all jwalterweatherman logging is sent to. -var logger *Logger - -// Logger manages the recording of jwalterweatherman logs. It can write logs to -// a local, in-memory buffer or to an external worker. -type Logger struct { - threshold jww.Threshold - maxLogFileSize int - logListenerID uint64 - - listenChan chan []byte - mode atomic.Uint32 - processQuit chan struct{} - - cb *circbuf.Buffer - wm *worker.Manager -} - -// InitLogger initializes the logger. Include this in the init function in main. -func InitLogger() *Logger { - logger = NewLogger() - return logger -} +var logger Logger // GetLogger returns the Logger object, used to manager where logging is // recorded. -func GetLogger() *Logger { +func GetLogger() Logger { return logger } -// NewLogger creates a new Logger that begins storing the first -// DefaultInitThreshold log entries. If either the log file or log worker is -// enabled, then these logs are redirected to the set destination. If the -// channel fills up with no log recorder enabled, then the listener is disabled. -func NewLogger() *Logger { - lf := newLogger() - - // Add the log listener - lf.logListenerID = AddLogListener(lf.Listen) +type Logger interface { + // StopLogging stops log message writes. Once logging is stopped, it cannot + // be resumed and the log file cannot be recovered. + StopLogging() - jww.INFO.Printf("[LOG] Enabled initial log file listener in %s with ID %d "+ - "at threshold %s that can store %d entries", - lf.getMode(), lf.logListenerID, lf.Threshold(), cap(lf.listenChan)) + // GetFile returns the entire log file. + GetFile() []byte - return lf -} - -// newLogger initialises a Logger without adding it as a log listener. -func newLogger() *Logger { - lf := &Logger{ - threshold: DefaultInitThreshold, - listenChan: make(chan []byte, logListenerChanSize), - mode: atomic.Uint32{}, - processQuit: make(chan struct{}), - } - lf.setMode(initMode) + // Threshold returns the log level threshold used in the file. + Threshold() jww.Threshold - return lf -} + // MaxSize returns the maximum size, in bytes, of the log file before it + // rolls over and starts overwriting the oldest entries + MaxSize() int -// LogToFile starts logging to a local, in-memory log file. -func (l *Logger) LogToFile(threshold jww.Threshold, maxLogFileSize int) error { - err := l.prepare(threshold, maxLogFileSize, fileMode) - if err != nil { - return err - } - - b, err := circbuf.NewBuffer(int64(maxLogFileSize)) - if err != nil { - return err - } - l.cb = b - - sendLog := func(p []byte) { - if n, err2 := l.cb.Write(p); err2 != nil { - jww.ERROR.Printf( - "[LOG] Error writing log to circular buffer: %+v", err2) - } else if n != len(p) { - jww.ERROR.Printf( - "[LOG] Wrote %d bytes when %d bytes expected", n, len(p)) - } - } - go l.processLog(workerMode, sendLog, l.processQuit) + // Size returns the number of bytes written to the log file. + Size() int - return nil + // Worker returns the manager for the Javascript Worker object. If the + // worker has not been initialized, it returns nil. + Worker() *worker.Manager } -// LogToFileWorker starts a new worker that begins listening for logs and -// writing them to file. This function blocks until the worker has started. -func (l *Logger) LogToFileWorker(threshold jww.Threshold, maxLogFileSize int, - wasmJsPath, workerName string) error { - err := l.prepare(threshold, maxLogFileSize, workerMode) - if err != nil { - return err - } +// EnableLogging enables logging to the Javascript console and to a local or +// worker file buffer. This must be called only once at initialisation. +func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int, + workerScriptURL, workerName string) error { - // Create new worker manager, which will start the worker and wait until - // communication has been established - wm, err := worker.NewManager(wasmJsPath, workerName, false) - if err != nil { - return err - } - l.wm = wm - - // Register the callback used by the Javascript to request the log file. - // This prevents an error print when GetFileExtTag is not registered. - l.wm.RegisterCallback(GetFileExtTag, func([]byte) { - jww.DEBUG.Print("[LOG] Received file requested from external " + - "Javascript. Ignoring file.") - }) - - data, err := json.Marshal(l.maxLogFileSize) - if err != nil { - return err + var listeners []jww.LogListener + if logLevel > -1 { + // Overwrites setting the log level to INFO done in bindings so that the + // Javascript console can be used + ll := NewJsConsoleLogListener(logLevel) + listeners = append(listeners, ll.Listen) + jww.SetStdoutThreshold(jww.LevelFatal + 1) + jww.FEEDBACK.Printf("[LOG] Log level for console set to %s", logLevel) + } else { + jww.FEEDBACK.Print("[LOG] Disabling logging to console.") } - // Send message to initialize the log file listener - errChan := make(chan error) - l.wm.SendMessage(NewLogFileTag, data, func(data []byte) { - if len(data) > 0 { - errChan <- errors.New(string(data)) + if fileLogLevel > -1 { + maxLogFileSize := maxLogFileSizeMB * 1_000_000 + if workerScriptURL == "" { + fl, err := newFileLogger(fileLogLevel, maxLogFileSize) + if err != nil { + return errors.Wrap(err, "could not initialize logging to file") + } + listeners = append(listeners, fl.Listen) } else { - errChan <- nil - } - }) + wl, err := newWorkerLogger( + fileLogLevel, maxLogFileSize, workerScriptURL, workerName) + if err != nil { + return errors.Wrap(err, "could not initialize logging to worker file") + } - // Wait for worker to respond - select { - case err = <-errChan: - if err != nil { - return err + listeners = append(listeners, wl.Listen) } - case <-time.After(worker.ResponseTimeout): - return errors.Errorf("timed out after %s waiting for new log "+ - "file in worker to initialize", worker.ResponseTimeout) - } - jww.INFO.Printf("[LOG] Initialized log to file web worker %s.", workerName) - - sendLog := func(p []byte) { l.wm.SendMessage(WriteLogTag, p, nil) } - go l.processLog(workerMode, sendLog, l.processQuit) - - return nil -} - -// processLog processes the log messages sent to the listener channel and sends -// them to the appropriate recorder. -func (l *Logger) processLog(m mode, sendLog func(p []byte), quit chan struct{}) { - jww.INFO.Printf("[LOG] Starting log file processing thread in %s.", m) - - for { - select { - case <-quit: - jww.INFO.Printf("[LOG] Stopping log file processing thread.") - return - case p := <-l.listenChan: - go sendLog(p) - } - } -} - -// prepare sets the threshold, maxLogFileSize, and mode of the logger and -// prints a log message indicating this information. -func (l *Logger) prepare( - threshold jww.Threshold, maxLogFileSize int, m mode) error { - if m := l.getMode(); m != initMode { - return errors.Errorf("log already set to %s", m) - } else if threshold < jww.LevelTrace || threshold > jww.LevelFatal { - return errors.Errorf("log level of %d is invalid", threshold) - } - - l.threshold = threshold - l.maxLogFileSize = maxLogFileSize - l.setMode(m) - - msg := fmt.Sprintf("[LOG] Outputting log to file in %s of max size %d "+ - "with level %s", m, l.MaxSize(), l.Threshold()) - switch l.Threshold() { - case jww.LevelTrace: - fallthrough - case jww.LevelDebug: - fallthrough - case jww.LevelInfo: - jww.INFO.Print(msg) - case jww.LevelWarn: - jww.WARN.Print(msg) - case jww.LevelError: - jww.ERROR.Print(msg) - case jww.LevelCritical: - jww.CRITICAL.Print(msg) - case jww.LevelFatal: - jww.FATAL.Print(msg) + js.Global().Set("GetLogger", js.FuncOf(GetLoggerJS)) } + jww.SetLogListeners(listeners...) return nil } -// StopLogging stops the logging of log messages and disables the log listener. -// If the log worker is running, it is terminated. Once logging is stopped, it -// cannot be resumed the log file cannot be recovered. -func (l *Logger) StopLogging() { - jww.DEBUG.Printf("[LOG] Removing log listener with ID %d", l.logListenerID) - RemoveLogListener(l.logListenerID) - - switch l.getMode() { - case workerMode: - l.wm.Stop() - jww.DEBUG.Printf("[LOG] Terminated log worker.") - case fileMode: - jww.DEBUG.Printf("[LOG] Reset circular buffer.") - l.cb.Reset() - } - - select { - case l.processQuit <- struct{}{}: - jww.DEBUG.Printf("[LOG] Sent quit channel to log process.") - default: - jww.DEBUG.Printf("[LOG] Failed to stop log processes.") - } -} - -// GetFile returns the entire log file. -// -// If the log file is listening locally, it returns it from the local buffer. If -// it is listening from the worker, it blocks until the file is returned. -func (l *Logger) GetFile() []byte { - switch l.getMode() { - case fileMode: - return l.cb.Bytes() - case workerMode: - fileChan := make(chan []byte) - l.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data }) - - select { - case file := <-fileChan: - return file - case <-time.After(worker.ResponseTimeout): - jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ - "file from worker", worker.ResponseTimeout) - return nil - } - default: - return nil - } -} - -// Threshold returns the log level threshold used in the file. -func (l *Logger) Threshold() jww.Threshold { - return l.threshold -} - -// MaxSize returns the max size, in bytes, that the log file is allowed to be. -func (l *Logger) MaxSize() int { - return l.maxLogFileSize -} - -// Size returns the current size, in bytes, written to the log file. -// -// If the log file is listening locally, it returns it from the local buffer. If -// it is listening from the worker, it blocks until the size is returned. -func (l *Logger) Size() int { - switch l.getMode() { - case fileMode: - return int(l.cb.Size()) - case workerMode: - sizeChan := make(chan []byte) - l.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data }) - - select { - case data := <-sizeChan: - return int(jww.Threshold(binary.LittleEndian.Uint64(data))) - case <-time.After(worker.ResponseTimeout): - jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ - "file size from worker", worker.ResponseTimeout) - return 0 - } - default: - return 0 - } -} - -//////////////////////////////////////////////////////////////////////////////// -// JWW Listener // -//////////////////////////////////////////////////////////////////////////////// - -// Listen is called for every logging event. This function adheres to the -// [jwalterweatherman.LogListener] type. -func (l *Logger) Listen(t jww.Threshold) io.Writer { - if t < l.threshold { - return nil - } - - return l -} - -// Write sends the bytes to the listener channel. It always returns the length -// of p and a nil error. This function adheres to the io.Writer interface. -func (l *Logger) Write(p []byte) (n int, err error) { - select { - case l.listenChan <- append([]byte{}, p...): - default: - jww.ERROR.Printf( - "[LOG] Logger channel filled. Log file recording stopping.") - l.StopLogging() - return 0, errors.Errorf( - "Logger channel filled. Log file recording stopping.") - } - return len(p), nil -} - -//////////////////////////////////////////////////////////////////////////////// -// Log File Mode // -//////////////////////////////////////////////////////////////////////////////// - -// mode represents the state of the Logger. -type mode uint32 - -const ( - initMode mode = iota - fileMode - workerMode -) - -func (l *Logger) setMode(m mode) { l.mode.Store(uint32(m)) } -func (l *Logger) getMode() mode { return mode(l.mode.Load()) } - -// String returns a human-readable representation of the mode for logging and -// debugging. This function adheres to the fmt.Stringer interface. -func (m mode) String() string { - switch m { - case initMode: - return "uninitialized mode" - case fileMode: - return "file mode" - case workerMode: - return "worker mode" - default: - return "invalid mode: " + strconv.Itoa(int(m)) - } -} - //////////////////////////////////////////////////////////////////////////////// // Javascript Bindings // //////////////////////////////////////////////////////////////////////////////// @@ -396,142 +113,98 @@ func (m mode) String() string { // Returns: // - A Javascript representation of the [Logger] object. func GetLoggerJS(js.Value, []js.Value) any { - return newLoggerJS(GetLogger()) + // l := GetLogger() + // if l != nil { + // return newLoggerJS(LoggerJS{GetLogger()}) + // } + // return js.Null() + return newLoggerJS(LoggerJS{GetLogger()}) +} + +type LoggerJS struct { + api Logger } // newLoggerJS creates a new Javascript compatible object (map[string]any) that // matches the [Logger] structure. -func newLoggerJS(lfw *Logger) map[string]any { +func newLoggerJS(l LoggerJS) map[string]any { logFileWorker := map[string]any{ - "LogToFile": js.FuncOf(lfw.LogToFileJS), - "LogToFileWorker": js.FuncOf(lfw.LogToFileWorkerJS), - "StopLogging": js.FuncOf(lfw.StopLoggingJS), - "GetFile": js.FuncOf(lfw.GetFileJS), - "Threshold": js.FuncOf(lfw.ThresholdJS), - "MaxSize": js.FuncOf(lfw.MaxSizeJS), - "Size": js.FuncOf(lfw.SizeJS), - "Worker": js.FuncOf(lfw.WorkerJS), + "StopLogging": js.FuncOf(l.StopLogging), + "GetFile": js.FuncOf(l.GetFile), + "Threshold": js.FuncOf(l.Threshold), + "MaxSize": js.FuncOf(l.MaxSize), + "Size": js.FuncOf(l.Size), + "Worker": js.FuncOf(l.Worker), } return logFileWorker } -// LogToFileJS starts logging to a local, in-memory log file. -// -// Parameters: -// - args[0] - Log level (int). -// - args[1] - Max log file size, in bytes (int). -// -// Returns: -// - Throws a TypeError if starting the log file fails. -func (l *Logger) LogToFileJS(_ js.Value, args []js.Value) any { - threshold := jww.Threshold(args[0].Int()) - maxLogFileSize := args[1].Int() - - err := l.LogToFile(threshold, maxLogFileSize) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil - } - - return nil -} - -// LogToFileWorkerJS starts a new worker that begins listening for logs and -// writing them to file. This function blocks until the worker has started. -// -// Parameters: -// - args[0] - Log level (int). -// - args[1] - Max log file size, in bytes (int). -// - args[2] - Path to Javascript start file for the worker WASM (string). -// - args[3] - Name of the worker (used in logs) (string). -// -// Returns a promise: -// - Resolves to nothing on success (void). -// - Rejected with an error if starting the worker fails. -func (l *Logger) LogToFileWorkerJS(_ js.Value, args []js.Value) any { - threshold := jww.Threshold(args[0].Int()) - maxLogFileSize := args[1].Int() - wasmJsPath := args[2].String() - workerName := args[3].String() - - promiseFn := func(resolve, reject func(args ...any) js.Value) { - err := l.LogToFileWorker( - threshold, maxLogFileSize, wasmJsPath, workerName) - if err != nil { - reject(utils.JsTrace(err)) - } else { - resolve() - } - } - - return utils.CreatePromise(promiseFn) -} - -// StopLoggingJS stops the logging of log messages and disables the log +// StopLogging stops the logging of log messages and disables the log // listener. If the log worker is running, it is terminated. Once logging is // stopped, it cannot be resumed the log file cannot be recovered. -func (l *Logger) StopLoggingJS(js.Value, []js.Value) any { - l.StopLogging() +func (l *LoggerJS) StopLogging(js.Value, []js.Value) any { + l.api.StopLogging() return nil } -// GetFileJS returns the entire log file. +// GetFile returns the entire log file. // // If the log file is listening locally, it returns it from the local buffer. If // it is listening from the worker, it blocks until the file is returned. // // Returns a promise: // - Resolves to the log file contents (string). -func (l *Logger) GetFileJS(js.Value, []js.Value) any { +func (l *LoggerJS) GetFile(js.Value, []js.Value) any { promiseFn := func(resolve, _ func(args ...any) js.Value) { - resolve(string(l.GetFile())) + resolve(string(l.api.GetFile())) } return utils.CreatePromise(promiseFn) } -// ThresholdJS returns the log level threshold used in the file. +// Threshold returns the log level threshold used in the file. // // Returns: // - Log level (int). -func (l *Logger) ThresholdJS(js.Value, []js.Value) any { - return int(l.Threshold()) +func (l *LoggerJS) Threshold(js.Value, []js.Value) any { + return int(l.api.Threshold()) } -// MaxSizeJS returns the max size, in bytes, that the log file is allowed to be. +// MaxSize returns the max size, in bytes, that the log file is allowed to be. // // Returns: // - Max file size (int). -func (l *Logger) MaxSizeJS(js.Value, []js.Value) any { - return l.MaxSize() +func (l *LoggerJS) MaxSize(js.Value, []js.Value) any { + return l.api.MaxSize() } -// SizeJS returns the current size, in bytes, written to the log file. +// Size returns the current size, in bytes, written to the log file. // // If the log file is listening locally, it returns it from the local buffer. If // it is listening from the worker, it blocks until the size is returned. // // Returns a promise: // - Resolves to the current file size (int). -func (l *Logger) SizeJS(js.Value, []js.Value) any { +func (l *LoggerJS) Size(js.Value, []js.Value) any { promiseFn := func(resolve, _ func(args ...any) js.Value) { - resolve(l.Size()) + resolve(l.api.Size()) } return utils.CreatePromise(promiseFn) } -// WorkerJS returns the web worker object. +// Worker returns the web worker object. // // Returns: // - Javascript worker object. If the worker has not been initialized, it // returns null. -func (l *Logger) WorkerJS(js.Value, []js.Value) any { - if l.getMode() == workerMode { - return l.wm.GetWorker() +func (l *LoggerJS) Worker(js.Value, []js.Value) any { + wm := l.api.Worker() + if wm == nil { + return js.Null() } - return js.Null() + return wm.GetWorker() } diff --git a/logging/logger_test.go b/logging/logger_test.go deleted file mode 100644 index 0b5267be9cabaf995ddc8fcdba4b5d3ec8eea133..0000000000000000000000000000000000000000 --- a/logging/logger_test.go +++ /dev/null @@ -1,172 +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 logging - -import ( - "bytes" - "fmt" - jww "github.com/spf13/jwalterweatherman" - "testing" -) - -// Tests InitLogger -func TestInitLogger(t *testing.T) { -} - -// Tests GetLogger -func TestGetLogger(t *testing.T) { -} - -// Tests NewLogger -func TestNewLogger(t *testing.T) { -} - -// Tests Logger.LogToFile -func TestLogger_LogToFile(t *testing.T) { - jww.SetStdoutThreshold(jww.LevelTrace) - l := NewLogger() - - err := l.LogToFile(jww.LevelTrace, 50000000) - if err != nil { - t.Fatalf("Failed to LogToFile: %+v", err) - } - - jww.INFO.Printf("test") - - file := l.cb.Bytes() - fmt.Printf("file:----------------------------\n%s\n---------------------------------\n", file) -} - -// Tests Logger.LogToFileWorker -func TestLogger_LogToFileWorker(t *testing.T) { -} - -// Tests Logger.processLog -func TestLogger_processLog(t *testing.T) { -} - -// Tests Logger.prepare -func TestLogger_prepare(t *testing.T) { -} - -// Tests Logger.StopLogging -func TestLogger_StopLogging(t *testing.T) { -} - -// Tests Logger.GetFile -func TestLogger_GetFile(t *testing.T) { -} - -// Tests Logger.Threshold -func TestLogger_Threshold(t *testing.T) { -} - -// Tests Logger.MaxSize -func TestLogger_MaxSize(t *testing.T) { -} - -// Tests Logger.Size -func TestLogger_Size(t *testing.T) { -} - -// Tests Logger.Listen -func TestLogger_Listen(t *testing.T) { - - // l := newLogger() - -} - -// Tests that Logger.Write can fill the listenChan channel completely and that -// all messages are received in the order they were added. -func TestLogger_Write(t *testing.T) { - l := newLogger() - expectedLogs := make([][]byte, logListenerChanSize) - - for i := range expectedLogs { - p := []byte( - fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize)) - expectedLogs[i] = p - n, err := l.Listen(jww.LevelError).Write(p) - if err != nil { - t.Errorf("Received impossible error (%d): %+v", i, err) - } else if n != len(p) { - t.Errorf("Received incorrect bytes written (%d)."+ - "\nexpected: %d\nreceived: %d", i, len(p), n) - } - } - - for i, expected := range expectedLogs { - select { - case received := <-l.listenChan: - if !bytes.Equal(expected, received) { - t.Errorf("Received unexpected meessage (%d)."+ - "\nexpected: %q\nreceived: %q", i, expected, received) - } - default: - t.Errorf("Failed to read from channel.") - } - } -} - -// Error path: Tests that Logger.Write returns an error when the listener -// channel is full. -func TestLogger_Write_ChannelFilledError(t *testing.T) { - l := newLogger() - expectedLogs := make([][]byte, logListenerChanSize) - - for i := range expectedLogs { - p := []byte( - fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize)) - expectedLogs[i] = p - n, err := l.Listen(jww.LevelError).Write(p) - if err != nil { - t.Errorf("Received impossible error (%d): %+v", i, err) - } else if n != len(p) { - t.Errorf("Received incorrect bytes written (%d)."+ - "\nexpected: %d\nreceived: %d", i, len(p), n) - } - } - - _, err := l.Write([]byte("test")) - if err == nil { - t.Error("Failed to receive error when the chanel should be full.") - } -} - -// Tests that Logger.getMode gets the same value set with Logger.setMode. -func TestLogger_setMode_getMode(t *testing.T) { - l := newLogger() - - for i, m := range []mode{initMode, fileMode, workerMode, 12} { - l.setMode(m) - received := l.getMode() - if m != received { - t.Errorf("Received wrong mode (%d).\nexpected: %s\nreceived: %s", - i, m, received) - } - } - -} - -// Unit test of mode.String. -func Test_mode_String(t *testing.T) { - for m, expected := range map[mode]string{ - initMode: "uninitialized mode", - fileMode: "file mode", - workerMode: "worker mode", - 12: "invalid mode: 12", - } { - s := m.String() - if s != expected { - t.Errorf("Wrong string for mode %d.\nexpected: %s\nreceived: %s", - m, expected, s) - } - } -} diff --git a/logging/workerLogger.go b/logging/workerLogger.go new file mode 100644 index 0000000000000000000000000000000000000000..bfac4703943c94956a8dc6501b722e7d0e5cad62 --- /dev/null +++ b/logging/workerLogger.go @@ -0,0 +1,162 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 logging + +import ( + "encoding/binary" + "encoding/json" + "io" + "math" + "time" + + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/xxdk-wasm/worker" +) + +// TODO: add ability to import worker so that multiple threads can send logs: https://stackoverflow.com/questions/8343781/how-to-do-worker-to-worker-communication + +// workerLogger manages the recording of jwalterweatherman logs to the in-memory +// file buffer in a remote Worker thread. +type workerLogger struct { + threshold jww.Threshold + maxLogFileSize int + wm *worker.Manager +} + +// newWorkerLogger starts logging to an in-memory log file in a remote Worker +// at the specified threshold. Returns a [workerLogger] that can be used to get +// the log file. +func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int, + wasmJsPath, workerName string) (*workerLogger, error) { + // Create new worker manager, which will start the worker and wait until + // communication has been established + wm, err := worker.NewManager(wasmJsPath, workerName, false) + if err != nil { + return nil, err + } + + wl := &workerLogger{ + threshold: threshold, + maxLogFileSize: maxLogFileSize, + wm: wm, + } + + // Register the callback used by the Javascript to request the log file. + // This prevents an error print when GetFileExtTag is not registered. + wl.wm.RegisterCallback(GetFileExtTag, func([]byte) { + jww.DEBUG.Print("[LOG] Received file requested from external " + + "Javascript. Ignoring file.") + }) + + data, err := json.Marshal(wl.maxLogFileSize) + if err != nil { + return nil, err + } + + // Send message to initialize the log file listener + errChan := make(chan error) + wl.wm.SendMessage(NewLogFileTag, data, func(data []byte) { + if len(data) > 0 { + errChan <- errors.New(string(data)) + } else { + errChan <- nil + } + }) + + // Wait for worker to respond + select { + case err = <-errChan: + if err != nil { + return nil, err + } + case <-time.After(worker.ResponseTimeout): + return nil, errors.Errorf("timed out after %s waiting for new log "+ + "file in worker to initialize", worker.ResponseTimeout) + } + + jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level "+ + "%s using web worker %s", wl.maxLogFileSize, wl.threshold, workerName) + + logger = wl + return wl, nil +} + +// Write adheres to the io.Writer interface and sends the log entries to the +// worker to be added to the file buffer. Always returns the length of p and +// nil. All errors are printed to the log. +func (wl *workerLogger) Write(p []byte) (n int, err error) { + wl.wm.SendMessage(WriteLogTag, p, nil) + return len(p), nil +} + +// Listen adheres to the [jwalterweatherman.LogListener] type and returns the +// log writer when the threshold is within the set threshold limit. +func (wl *workerLogger) Listen(threshold jww.Threshold) io.Writer { + if threshold < wl.threshold { + return nil + } + return wl +} + +// StopLogging stops log message writes and terminates the worker. Once logging +// is stopped, it cannot be resumed and the log file cannot be recovered. +func (wl *workerLogger) StopLogging() { + wl.threshold = math.MaxInt + + wl.wm.Stop() + jww.DEBUG.Printf("[LOG] Terminated log worker.") +} + +// GetFile returns the entire log file. +func (wl *workerLogger) GetFile() []byte { + fileChan := make(chan []byte) + wl.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data }) + + select { + case file := <-fileChan: + return file + case <-time.After(worker.ResponseTimeout): + jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ + "file from worker", worker.ResponseTimeout) + return nil + } +} + +// Threshold returns the log level threshold used in the file. +func (wl *workerLogger) Threshold() jww.Threshold { + return wl.threshold +} + +// MaxSize returns the max size, in bytes, that the log file is allowed to be. +func (wl *workerLogger) MaxSize() int { + return wl.maxLogFileSize +} + +// Size returns the number of bytes written to the log file. +func (wl *workerLogger) Size() int { + sizeChan := make(chan []byte) + wl.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data }) + + select { + case data := <-sizeChan: + return int(binary.LittleEndian.Uint64(data)) + case <-time.After(worker.ResponseTimeout): + jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ + "file size from worker", worker.ResponseTimeout) + return 0 + } +} + +// Worker returns the manager for the Javascript Worker object. +func (wl *workerLogger) Worker() *worker.Manager { + return wl.wm +} diff --git a/logging/workerThread/logFileWorker.js b/logging/workerThread/logFileWorker.js index 159bfaa0d919a4f0cb2758af48d80c65891e7820..ed246f62563f89645f95fe13c715a81b60aa756b 100644 --- a/logging/workerThread/logFileWorker.js +++ b/logging/workerThread/logFileWorker.js @@ -7,11 +7,15 @@ importScripts('wasm_exec.js'); +const isReady = new Promise((resolve) => { + self.onWasmInitialized = resolve; +}); + const go = new Go(); const binPath = 'xxdk-logFileWorker.wasm' -WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => { +WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); - LogLevel(1); + await isReady; }).catch((err) => { console.error(err); }); \ No newline at end of file diff --git a/logging/workerThread/main.go b/logging/workerThread/main.go index 91b059f3da1195b0ff244027608a0f8a98482fed..1a9a31a0ca39ee684b4427eef03fc89f4a407b8c 100644 --- a/logging/workerThread/main.go +++ b/logging/workerThread/main.go @@ -13,25 +13,21 @@ import ( "encoding/binary" "encoding/json" "fmt" + "os" + "syscall/js" + "github.com/armon/circbuf" "github.com/pkg/errors" + "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/worker" - "syscall/js" ) // SEMVER is the current semantic version of the xxDK Logger web worker. const SEMVER = "0.1.0" -func init() { - // Set up Javascript console listener set at level INFO - ll := logging.NewJsConsoleLogListener(jww.LevelDebug) - logging.AddLogListener(ll.Listen) - jww.SetStdoutThreshold(jww.LevelFatal + 1) - jww.INFO.Printf("xxDK Logger web worker version: v%s", SEMVER) -} - // workerLogFile manages communication with the main thread and writing incoming // logging messages to the log file. type workerLogFile struct { @@ -40,17 +36,60 @@ type workerLogFile struct { } func main() { - jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.") + // Set to os.Args because the default is os.Args[1:] and in WASM, args start + // at 0, not 1. + LoggerCmd.SetArgs(os.Args) + + err := LoggerCmd.Execute() + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +var LoggerCmd = &cobra.Command{ + Use: "Logger", + Short: "Web worker buffer file logger", + Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]", + Run: func(cmd *cobra.Command, args []string) { + // Start logger first to capture all logging events + err := logging.EnableLogging(logLevel, -1, 0, "", "") + if err != nil { + fmt.Printf( + "Failed to intialize logging in logging worker: %+v", err) + os.Exit(1) + } - js.Global().Set("LogLevel", js.FuncOf(logging.LogLevelJS)) + jww.INFO.Printf("xxDK Logger web worker version: v%s", SEMVER) - wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)} + jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.") - wlf.registerCallbacks() + wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)} - wlf.wtm.SignalReady() - <-make(chan bool) - fmt.Println("[WW] Closing xxDK WebAssembly Log Worker.") + wlf.registerCallbacks() + + wlf.wtm.SignalReady() + + // Indicate to the Javascript caller that the WASM is ready by resolving + // a promise created by the caller. + js.Global().Get("onWasmInitialized").Invoke() + + <-make(chan bool) + fmt.Println("[WW] Closing xxDK WebAssembly Log Worker.") + os.Exit(0) + }, +} + +var ( + logLevel jww.Threshold +) + +func init() { + // Initialize all startup flags + LoggerCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + "Sets the log level output when outputting to the Javascript console. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") } // registerCallbacks registers all the necessary callbacks for the main thread diff --git a/main.go b/main.go index c109ff54f41fc0c3acb34bd7d3c848013237360c..267bb55bb812d1950c412a51337464cd35f39b36 100644 --- a/main.go +++ b/main.go @@ -10,38 +10,76 @@ package main import ( - "gitlab.com/elixxir/xxdk-wasm/logging" + "fmt" "os" "syscall/js" + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/wasm-utils/utils" + "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/storage" - "gitlab.com/elixxir/xxdk-wasm/utils" "gitlab.com/elixxir/xxdk-wasm/wasm" ) -func init() { - // Start logger first to capture all logging events - logging.InitLogger() - - // Overwrites setting the log level to INFO done in bindings so that the - // Javascript console can be used - ll := logging.NewJsConsoleLogListener(jww.LevelInfo) - logging.AddLogListener(ll.Listen) - jww.SetStdoutThreshold(jww.LevelFatal + 1) +func main() { + // Set to os.Args because the default is os.Args[1:] and in WASM, args start + // at 0, not 1. + wasmCmd.SetArgs(os.Args) - // Check that the WASM binary version is correct - err := storage.CheckAndStoreVersions() + err := wasmCmd.Execute() if err != nil { - jww.FATAL.Panicf("WASM binary version error: %+v", err) + fmt.Println(err) + os.Exit(1) } } -func main() { - jww.INFO.Printf("Starting xxDK WebAssembly bindings.") +var wasmCmd = &cobra.Command{ + Use: "xxdk-wasm", + Short: "WebAssembly bindings for xxDK.", + Example: "const go = new Go();\ngo.argv = [\"--logLevel=1\"]", + Run: func(cmd *cobra.Command, args []string) { + // Start logger first to capture all logging events + err := logging.EnableLogging(logLevel, fileLogLevel, maxLogFileSizeMB, + workerScriptURL, workerName) + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } + + // Check that the WASM binary version is correct + err = storage.CheckAndStoreVersions() + if err != nil { + jww.FATAL.Panicf("WASM binary version error: %+v", err) + } + + // Enable all top level bindings functions + setGlobals() + + // Indicate to the Javascript caller that the WASM is ready by resolving + // a promise created by the caller, as shown below: + // + // let isReady = new Promise((resolve) => { + // window.onWasmInitialized = resolve; + // }); + // + // const go = new Go(); + // go.run(result.instance); + // await isReady; + // + // Source: https://github.com/golang/go/issues/49710#issuecomment-986484758 + js.Global().Get("onWasmInitialized").Invoke() + + <-make(chan bool) + os.Exit(0) + }, +} - // logging/worker.go - js.Global().Set("GetLogger", js.FuncOf(logging.GetLoggerJS)) +// setGlobals enables all global functions to be accessible to Javascript. +func setGlobals() { + jww.INFO.Printf("Starting xxDK WebAssembly bindings.") // storage/password.go js.Global().Set("GetOrInitPassword", js.FuncOf(storage.GetOrInitPassword)) @@ -62,6 +100,11 @@ func main() { js.Global().Set("InitializeBackup", js.FuncOf(wasm.InitializeBackup)) js.Global().Set("ResumeBackup", js.FuncOf(wasm.ResumeBackup)) + // wasm/notifications.go + js.Global().Set("LoadNotifications", js.FuncOf(wasm.LoadNotifications)) + js.Global().Set("LoadNotificationsDummy", + js.FuncOf(wasm.LoadNotificationsDummy)) + // wasm/channels.go js.Global().Set("GenerateChannelIdentity", js.FuncOf(wasm.GenerateChannelIdentity)) @@ -89,10 +132,14 @@ func main() { js.Global().Set("GetShareUrlType", js.FuncOf(wasm.GetShareUrlType)) js.Global().Set("ValidForever", js.FuncOf(wasm.ValidForever)) js.Global().Set("IsNicknameValid", js.FuncOf(wasm.IsNicknameValid)) + js.Global().Set("GetNotificationReportsForMe", + js.FuncOf(wasm.GetNotificationReportsForMe)) js.Global().Set("GetNoMessageErr", js.FuncOf(wasm.GetNoMessageErr)) js.Global().Set("CheckNoMessageErr", js.FuncOf(wasm.CheckNoMessageErr)) js.Global().Set("NewChannelsDatabaseCipher", js.FuncOf(wasm.NewChannelsDatabaseCipher)) + js.Global().Set("GetNotificationReportsForMe", + js.FuncOf(wasm.GetNotificationReportsForMe)) // wasm/dm.go js.Global().Set("InitChannelsFileTransfer", @@ -110,6 +157,8 @@ func main() { // 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.LoadSynchronizedCmix)) // wasm/delivery.go js.Global().Set("SetDashboardURL", js.FuncOf(wasm.SetDashboardURL)) @@ -157,7 +206,6 @@ func main() { js.FuncOf(wasm.GetFactsFromContact)) // wasm/logging.go - js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel)) js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter)) js.Global().Set("EnableGrpcLogs", js.FuncOf(wasm.EnableGrpcLogs)) @@ -194,6 +242,8 @@ func main() { js.Global().Set("TransmitSingleUse", js.FuncOf(wasm.TransmitSingleUse)) js.Global().Set("Listen", js.FuncOf(wasm.Listen)) + // wasm/sync.go + // wasm/timeNow.go js.Global().Set("SetTimeSource", js.FuncOf(wasm.SetTimeSource)) js.Global().Set("SetOffset", js.FuncOf(wasm.SetOffset)) @@ -212,7 +262,32 @@ func main() { js.Global().Set("GetClientDependencies", js.FuncOf(wasm.GetClientDependencies)) js.Global().Set("GetWasmSemanticVersion", js.FuncOf(wasm.GetWasmSemanticVersion)) js.Global().Set("GetXXDKSemanticVersion", js.FuncOf(wasm.GetXXDKSemanticVersion)) +} + +var ( + logLevel, fileLogLevel jww.Threshold + maxLogFileSizeMB int + workerScriptURL, workerName string +) - <-make(chan bool) - os.Exit(0) +func init() { + // Initialize all startup flags + wasmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + "Sets the log level output when outputting to the Javascript console. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") + wasmCmd.Flags().IntVarP((*int)(&fileLogLevel), "fileLogLevel", "m", -1, + "The log level when outputting to the file buffer. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") + wasmCmd.Flags().IntVarP(&maxLogFileSizeMB, "maxLogFileSize", "s", 5, + "Max file size, in MB, for the file buffer before it rolls over "+ + "and starts overwriting the oldest entries.") + wasmCmd.Flags().StringVarP(&workerScriptURL, "workerScriptURL", "w", "", + "URL to the script that executes the worker. If set, it enables the "+ + "saving of log file to buffer in Worker instead of in the local "+ + "thread. This allows logging to be available after the main WASM "+ + "thread crashes.") + wasmCmd.Flags().StringVar(&workerName, "workerName", "xxdkLogFileWorker", + "Name of the logger worker.") } diff --git a/storage/indexedDbEncryptionTrack.go b/storage/indexedDbEncryptionTrack.go index d568a63787490ea0979a189191272da70d957e05..bb3f049b8adeef1e2f69b535e79285338f5107bc 100644 --- a/storage/indexedDbEncryptionTrack.go +++ b/storage/indexedDbEncryptionTrack.go @@ -12,6 +12,8 @@ package storage import ( "github.com/pkg/errors" "os" + + "gitlab.com/elixxir/wasm-utils/storage" ) // Key to store if the database is encrypted or not @@ -22,12 +24,15 @@ const databaseEncryptionToggleKey = "xxdkWasmDatabaseEncryptionToggle/" func StoreIndexedDbEncryptionStatus( databaseName string, encryptionStatus bool) ( loadedEncryptionStatus bool, err error) { - data, err := GetLocalStorage().GetItem( - databaseEncryptionToggleKey + databaseName) + ls := storage.GetLocalStorage() + data, err := ls.Get(databaseEncryptionToggleKey + databaseName) if err != nil { if errors.Is(err, os.ErrNotExist) { - GetLocalStorage().SetItem( - databaseEncryptionToggleKey+databaseName, []byte{1}) + keyName := databaseEncryptionToggleKey + databaseName + if err = ls.Set(keyName, []byte{1}); err != nil { + return false, + errors.Wrapf(err, "localStorage: failed to set %q", keyName) + } return encryptionStatus, nil } else { return false, err diff --git a/storage/indexedDbList.go b/storage/indexedDbList.go index a736984f0ef742d96e7a63238ea6ebfbdc25d9f9..8917fcdbf3c413d306bd6cc0687d2af80b441992 100644 --- a/storage/indexedDbList.go +++ b/storage/indexedDbList.go @@ -11,8 +11,11 @@ package storage import ( "encoding/json" - "github.com/pkg/errors" "os" + + "github.com/pkg/errors" + + "gitlab.com/elixxir/wasm-utils/storage" ) const indexedDbListKey = "xxDkWasmIndexedDbList" @@ -20,7 +23,7 @@ const indexedDbListKey = "xxDkWasmIndexedDbList" // GetIndexedDbList returns the list of stored indexedDb databases. func GetIndexedDbList() (map[string]struct{}, error) { list := make(map[string]struct{}) - listBytes, err := GetLocalStorage().GetItem(indexedDbListKey) + listBytes, err := storage.GetLocalStorage().Get(indexedDbListKey) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } else if err == nil { @@ -47,7 +50,11 @@ func StoreIndexedDb(databaseName string) error { return err } - GetLocalStorage().SetItem(indexedDbListKey, listBytes) + err = storage.GetLocalStorage().Set(indexedDbListKey, listBytes) + if err != nil { + return errors.Wrapf(err, + "localStorage: failed to set %q", indexedDbListKey) + } return nil } diff --git a/storage/localStorage.go b/storage/localStorage.go deleted file mode 100644 index d54039c99c427640b3298c7aeb87dfb819d046be..0000000000000000000000000000000000000000 --- a/storage/localStorage.go +++ /dev/null @@ -1,209 +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 storage - -import ( - "encoding/base64" - "encoding/json" - jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" - "os" - "strings" - "syscall/js" -) - -// localStorageWasmPrefix is prefixed to every keyName saved to local storage by -// LocalStorage. It allows the identifications and deletion of keys only created -// by this WASM binary while ignoring keys made by other scripts on the same -// page. -const localStorageWasmPrefix = "xxdkWasmStorage/" - -// LocalStorage contains the js.Value representation of localStorage. -type LocalStorage struct { - // The Javascript value containing the localStorage object - v js.Value - - // The prefix appended to each key name. This is so that all keys created by - // this structure can be deleted without affecting other keys in local - // storage. - prefix string -} - -// jsStorage is the global that stores Javascript as window.localStorage. -// -// - Specification: -// https://html.spec.whatwg.org/multipage/webstorage.html#dom-localstorage-dev -// - Documentation: -// https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage -var jsStorage = newLocalStorage(localStorageWasmPrefix) - -// newLocalStorage creates a new LocalStorage object with the specified prefix. -func newLocalStorage(prefix string) *LocalStorage { - return &LocalStorage{ - v: js.Global().Get("localStorage"), - prefix: prefix, - } -} - -// GetLocalStorage returns Javascript's local storage. -func GetLocalStorage() *LocalStorage { - return jsStorage -} - -// GetItem returns a key's value from the local storage given its name. Returns -// os.ErrNotExist if the key does not exist. Underneath, it calls -// localStorage.GetItem(). -// -// - Specification: -// https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-getitem-dev -// - Documentation: -// https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem -func (ls *LocalStorage) GetItem(keyName string) ([]byte, error) { - keyValue := ls.getItem(ls.prefix + keyName) - if keyValue.IsNull() { - return nil, os.ErrNotExist - } - - decodedKeyValue, err := base64.StdEncoding.DecodeString(keyValue.String()) - if err != nil { - return nil, err - } - - return decodedKeyValue, nil -} - -// SetItem adds a key's value to local storage given its name. Underneath, it -// calls localStorage.SetItem(). -// -// - Specification: -// https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-setitem-dev -// - Documentation: -// https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem -func (ls *LocalStorage) SetItem(keyName string, keyValue []byte) { - encodedKeyValue := base64.StdEncoding.EncodeToString(keyValue) - ls.setItem(ls.prefix+keyName, encodedKeyValue) -} - -// RemoveItem removes a key's value from local storage given its name. If there -// is no item with the given key, this function does nothing. Underneath, it -// calls localStorage.RemoveItem(). -// -// - Specification: -// https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-removeitem-dev -// - Documentation: -// https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem -func (ls *LocalStorage) RemoveItem(keyName string) { - ls.removeItem(ls.prefix + keyName) -} - -// Clear clears all the keys in storage. Underneath, it calls -// localStorage.clear(). -// -// - Specification: -// https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-clear-dev -// - Documentation: -// https://developer.mozilla.org/en-US/docs/Web/API/Storage/clear -func (ls *LocalStorage) Clear() { - ls.clear() -} - -// ClearPrefix clears all keys with the given prefix. Returns the number of -// keys cleared. -func (ls *LocalStorage) ClearPrefix(prefix string) int { - // Get a copy of all key names at once - keys := ls.keys() - - // Loop through each key - var n int - for i := 0; i < keys.Length(); i++ { - if v := keys.Index(i); !v.IsNull() { - keyName := strings.TrimPrefix(v.String(), ls.prefix) - if strings.HasPrefix(keyName, prefix) { - ls.removeItem(v.String()) - n++ - } - } - } - - return n -} - -// ClearWASM clears all the keys in storage created by WASM. Returns the number -// of keys cleared. -func (ls *LocalStorage) ClearWASM() int { - // Get a copy of all key names at once - keys := ls.keys() - - // Loop through each key - var n int - for i := 0; i < keys.Length(); i++ { - if v := keys.Index(i); !v.IsNull() { - keyName := v.String() - if strings.HasPrefix(keyName, ls.prefix) { - ls.RemoveItem(strings.TrimPrefix(keyName, ls.prefix)) - n++ - } - } - } - - return n -} - -// Key returns the name of the nth key in localStorage. Return os.ErrNotExist if -// the key does not exist. The order of keys is not defined. If there is no item -// with the given key, this function does nothing. Underneath, it calls -// localStorage.key(). -// -// - Specification: -// https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-key-dev -// - Documentation: -// https://developer.mozilla.org/en-US/docs/Web/API/Storage/key -func (ls *LocalStorage) Key(n int) (string, error) { - keyName := ls.key(n) - if keyName.IsNull() { - return "", os.ErrNotExist - } - - return strings.TrimPrefix(keyName.String(), ls.prefix), nil -} - -// Keys returns a list of all key names in local storage. -func (ls *LocalStorage) Keys() []string { - keyNamesJson := utils.JSON.Call("stringify", ls.keys()) - - var keyNames []string - err := json.Unmarshal([]byte(keyNamesJson.String()), &keyNames) - if err != nil { - jww.FATAL.Panicf( - "Failed to JSON unmarshal localStorage key name list: %+v", err) - } - - return keyNames -} - -// Length returns the number of keys in localStorage. Underneath, it accesses -// the property localStorage.length. -// -// - Specification: -// https://html.spec.whatwg.org/multipage/webstorage.html#dom-storage-key-dev -// - Documentation: -// https://developer.mozilla.org/en-US/docs/Web/API/Storage/length -func (ls *LocalStorage) Length() int { - return ls.length().Int() -} - -// Wrappers for Javascript Storage methods and properties. -func (ls *LocalStorage) getItem(keyName string) js.Value { return ls.v.Call("getItem", keyName) } -func (ls *LocalStorage) setItem(keyName, keyValue string) { ls.v.Call("setItem", keyName, keyValue) } -func (ls *LocalStorage) removeItem(keyName string) { ls.v.Call("removeItem", keyName) } -func (ls *LocalStorage) clear() { ls.v.Call("clear") } -func (ls *LocalStorage) key(n int) js.Value { return ls.v.Call("key", n) } -func (ls *LocalStorage) length() js.Value { return ls.v.Get("length") } -func (ls *LocalStorage) keys() js.Value { return utils.Object.Call("keys", ls.v) } diff --git a/storage/localStorage_test.go b/storage/localStorage_test.go deleted file mode 100644 index 20e424108af9b7ed64c9c4ba00e348b121beefc4..0000000000000000000000000000000000000000 --- a/storage/localStorage_test.go +++ /dev/null @@ -1,271 +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 storage - -import ( - "bytes" - "github.com/pkg/errors" - "math/rand" - "os" - "strconv" - "testing" -) - -// Tests that a value set with LocalStorage.SetItem and retrieved with -// LocalStorage.GetItem matches the original. -func TestLocalStorage_GetItem_SetItem(t *testing.T) { - values := map[string][]byte{ - "key1": []byte("key value"), - "key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, - "key3": {0, 49, 0, 0, 0, 38, 249, 93, 242, 189, 222, 32, 138, 248, 121, - 151, 42, 108, 82, 199, 163, 61, 4, 200, 140, 231, 225, 20, 35, 243, - 253, 161, 61, 2, 227, 208, 173, 183, 33, 66, 236, 107, 105, 119, 26, - 42, 44, 60, 109, 172, 38, 47, 220, 17, 129, 4, 234, 241, 141, 81, - 84, 185, 32, 120, 115, 151, 128, 196, 143, 117, 222, 78, 44, 115, - 109, 20, 249, 46, 158, 139, 231, 157, 54, 219, 141, 252}, - } - - for keyName, keyValue := range values { - jsStorage.SetItem(keyName, keyValue) - - loadedValue, err := jsStorage.GetItem(keyName) - if err != nil { - t.Errorf("Failed to load %q: %+v", keyName, err) - } - - if !bytes.Equal(keyValue, loadedValue) { - t.Errorf("Loaded value does not match original for %q"+ - "\nexpected: %q\nreceived: %q", keyName, keyValue, loadedValue) - } - } -} - -// Tests that LocalStorage.GetItem returns the error os.ErrNotExist when the key -// does not exist in storage. -func TestLocalStorage_GetItem_NotExistError(t *testing.T) { - _, err := jsStorage.GetItem("someKey") - if err == nil || !errors.Is(err, os.ErrNotExist) { - t.Errorf("Incorrect error for non existant key."+ - "\nexpected: %v\nreceived: %v", os.ErrNotExist, err) - } -} - -// Tests that LocalStorage.RemoveItem deletes a key from store and that it -// cannot be retrieved. -func TestLocalStorage_RemoveItem(t *testing.T) { - keyName := "key" - jsStorage.SetItem(keyName, []byte("value")) - jsStorage.RemoveItem(keyName) - - _, err := jsStorage.GetItem(keyName) - if err == nil || !errors.Is(err, os.ErrNotExist) { - t.Errorf("Failed to remove %q: %+v", keyName, err) - } -} - -// Tests that LocalStorage.Clear deletes all keys from storage. -func TestLocalStorage_Clear(t *testing.T) { - for i := 0; i < 10; i++ { - jsStorage.SetItem(strconv.Itoa(i), []byte(strconv.Itoa(i))) - } - - jsStorage.Clear() - - l := jsStorage.Length() - - if l > 0 { - t.Errorf("Clear did not delete all keys. Found %d keys.", l) - } -} - -// Tests that LocalStorage.ClearPrefix deletes only the keys with the given -// prefix. -func TestLocalStorage_ClearPrefix(t *testing.T) { - s := newLocalStorage("") - s.clear() - prng := rand.New(rand.NewSource(11)) - const numKeys = 10 - var yesPrefix, noPrefix []string - prefix := "keyNamePrefix/" - - for i := 0; i < numKeys; i++ { - keyName := "keyNum" + strconv.Itoa(i) - if prng.Intn(2) == 0 { - keyName = prefix + keyName - yesPrefix = append(yesPrefix, keyName) - } else { - noPrefix = append(noPrefix, keyName) - } - - s.SetItem(keyName, []byte(strconv.Itoa(i))) - } - - n := s.ClearPrefix(prefix) - if n != numKeys/2 { - t.Errorf("Incorrect number of keys.\nexpected: %d\nreceived: %d", - numKeys/2, n) - } - - for _, keyName := range noPrefix { - if _, err := s.GetItem(keyName); err != nil { - t.Errorf("Could not get keyName %q: %+v", keyName, err) - } - } - for _, keyName := range yesPrefix { - keyValue, err := s.GetItem(keyName) - if err == nil || !errors.Is(err, os.ErrNotExist) { - t.Errorf("Found keyName %q: %q", keyName, keyValue) - } - } -} - -// Tests that LocalStorage.ClearWASM deletes all the WASM keys from storage and -// does not remove any others -func TestLocalStorage_ClearWASM(t *testing.T) { - jsStorage.clear() - prng := rand.New(rand.NewSource(11)) - const numKeys = 10 - var yesPrefix, noPrefix []string - - for i := 0; i < numKeys; i++ { - keyName := "keyNum" + strconv.Itoa(i) - if prng.Intn(2) == 0 { - yesPrefix = append(yesPrefix, keyName) - jsStorage.SetItem(keyName, []byte(strconv.Itoa(i))) - } else { - noPrefix = append(noPrefix, keyName) - jsStorage.setItem(keyName, strconv.Itoa(i)) - } - } - - n := jsStorage.ClearWASM() - if n != numKeys/2 { - t.Errorf("Incorrect number of keys.\nexpected: %d\nreceived: %d", - numKeys/2, n) - } - - for _, keyName := range noPrefix { - if v := jsStorage.getItem(keyName); v.IsNull() { - t.Errorf("Could not get keyName %q.", keyName) - } - } - for _, keyName := range yesPrefix { - keyValue, err := jsStorage.GetItem(keyName) - if err == nil || !errors.Is(err, os.ErrNotExist) { - t.Errorf("Found keyName %q: %q", keyName, keyValue) - } - } -} - -// Tests that LocalStorage.Key return all added keys when looping through all -// indexes. -func TestLocalStorage_Key(t *testing.T) { - jsStorage.clear() - values := map[string][]byte{ - "key1": []byte("key value"), - "key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, - "key3": {0, 49, 0, 0, 0, 38, 249, 93}, - } - - for keyName, keyValue := range values { - jsStorage.SetItem(keyName, keyValue) - } - - numKeys := len(values) - for i := 0; i < numKeys; i++ { - keyName, err := jsStorage.Key(i) - if err != nil { - t.Errorf("No key found for index %d: %+v", i, err) - } - - if _, exists := values[keyName]; !exists { - t.Errorf("No key with name %q added to storage.", keyName) - } - delete(values, keyName) - } - - if len(values) != 0 { - t.Errorf("%d keys not read from storage: %q", len(values), values) - } -} - -// Tests that LocalStorage.Key returns the error os.ErrNotExist when the index -// is greater than or equal to the number of keys. -func TestLocalStorage_Key_NotExistError(t *testing.T) { - jsStorage.clear() - jsStorage.SetItem("key", []byte("value")) - - _, err := jsStorage.Key(1) - if err == nil || !errors.Is(err, os.ErrNotExist) { - t.Errorf("Incorrect error for non existant key index."+ - "\nexpected: %v\nreceived: %v", os.ErrNotExist, err) - } - - _, err = jsStorage.Key(2) - if err == nil || !errors.Is(err, os.ErrNotExist) { - t.Errorf("Incorrect error for non existant key index."+ - "\nexpected: %v\nreceived: %v", os.ErrNotExist, err) - } -} - -// Tests that LocalStorage.Length returns the correct Length when adding and -// removing various keys. -func TestLocalStorage_Length(t *testing.T) { - jsStorage.clear() - values := map[string][]byte{ - "key1": []byte("key value"), - "key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, - "key3": {0, 49, 0, 0, 0, 38, 249, 93}, - } - - i := 0 - for keyName, keyValue := range values { - jsStorage.SetItem(keyName, keyValue) - i++ - - if jsStorage.Length() != i { - t.Errorf("Incorrect length.\nexpected: %d\nreceived: %d", - i, jsStorage.Length()) - } - } - - i = len(values) - for keyName := range values { - jsStorage.RemoveItem(keyName) - i-- - - if jsStorage.Length() != i { - t.Errorf("Incorrect length.\nexpected: %d\nreceived: %d", - i, jsStorage.Length()) - } - } -} - -// Tests that LocalStorage.Keys return a list that contains all the added keys. -func TestLocalStorage_Keys(t *testing.T) { - s := newLocalStorage("") - s.clear() - values := map[string][]byte{ - "key1": []byte("key value"), - "key2": {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, - "key3": {0, 49, 0, 0, 0, 38, 249, 93}, - } - - for keyName, keyValue := range values { - s.SetItem(keyName, keyValue) - } - - keys := s.Keys() - for i, keyName := range keys { - if _, exists := values[keyName]; !exists { - t.Errorf("Key %q does not exist (%d).", keyName, i) - } - } -} diff --git a/storage/password.go b/storage/password.go index 36175e1002dbe3343a32fa41a9af239a9e13a7aa..7be49b310b54b7aa2764678f35d0b91550d2c491 100644 --- a/storage/password.go +++ b/storage/password.go @@ -12,16 +12,21 @@ package storage import ( "crypto/cipher" "encoding/json" - "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" - "gitlab.com/xx_network/crypto/csprng" - "golang.org/x/crypto/argon2" - "golang.org/x/crypto/blake2b" - "golang.org/x/crypto/chacha20poly1305" "io" "os" "syscall/js" + + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/chacha20poly1305" + + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/storage" + "gitlab.com/elixxir/wasm-utils/utils" + "gitlab.com/xx_network/crypto/csprng" ) // Data lengths. @@ -91,7 +96,7 @@ const ( func GetOrInitPassword(_ js.Value, args []js.Value) any { internalPassword, err := getOrInit(args[0].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -109,7 +114,7 @@ func GetOrInitPassword(_ js.Value, args []js.Value) any { func ChangeExternalPassword(_ js.Value, args []js.Value) any { err := changeExternalPassword(args[0].String(), args[1].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -130,7 +135,7 @@ func VerifyPassword(_ js.Value, args []js.Value) any { // getOrInit is the private function for GetOrInitPassword that is used for // testing. func getOrInit(externalPassword string) ([]byte, error) { - localStorage := GetLocalStorage() + localStorage := storage.GetLocalStorage() internalPassword, err := getInternalPassword(externalPassword, localStorage) if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -148,7 +153,7 @@ func getOrInit(externalPassword string) ([]byte, error) { // changeExternalPassword is the private function for ChangeExternalPassword // that is used for testing. func changeExternalPassword(oldExternalPassword, newExternalPassword string) error { - localStorage := GetLocalStorage() + localStorage := storage.GetLocalStorage() internalPassword, err := getInternalPassword( oldExternalPassword, localStorage) if err != nil { @@ -159,13 +164,17 @@ func changeExternalPassword(oldExternalPassword, newExternalPassword string) err if err != nil { return err } - localStorage.SetItem(saltKey, salt) + if err = localStorage.Set(saltKey, salt); err != nil { + return errors.Wrapf(err, "localStorage: failed to set %q", saltKey) + } key := deriveKey(newExternalPassword, salt, defaultParams()) encryptedInternalPassword := encryptPassword( internalPassword, key, csprng.NewSystemRNG()) - localStorage.SetItem(passwordKey, encryptedInternalPassword) + if err = localStorage.Set(passwordKey, encryptedInternalPassword); err != nil { + return errors.Wrapf(err, "localStorage: failed to set %q", passwordKey) + } return nil } @@ -173,14 +182,14 @@ func changeExternalPassword(oldExternalPassword, newExternalPassword string) err // verifyPassword is the private function for VerifyPassword that is used for // testing. func verifyPassword(externalPassword string) bool { - _, err := getInternalPassword(externalPassword, GetLocalStorage()) + _, err := getInternalPassword(externalPassword, storage.GetLocalStorage()) return err == nil } // initInternalPassword generates a new internal password, stores an encrypted // version in local storage, and returns it. func initInternalPassword(externalPassword string, - localStorage *LocalStorage, csprng io.Reader, + localStorage *storage.LocalStorage, csprng io.Reader, params argonParams) ([]byte, error) { internalPassword := make([]byte, internalPasswordLen) @@ -198,19 +207,28 @@ func initInternalPassword(externalPassword string, if err != nil { return nil, err } - localStorage.SetItem(saltKey, salt) + if err = localStorage.Set(saltKey, salt); err != nil { + return nil, + errors.Wrapf(err, "localStorage: failed to set %q", saltKey) + } // Store argon2 parameters paramsData, err := json.Marshal(params) if err != nil { return nil, err } - localStorage.SetItem(argonParamsKey, paramsData) + if err = localStorage.Set(argonParamsKey, paramsData); err != nil { + return nil, + errors.Wrapf(err, "localStorage: failed to set %q", argonParamsKey) + } key := deriveKey(externalPassword, salt, params) encryptedInternalPassword := encryptPassword(internalPassword, key, csprng) - localStorage.SetItem(passwordKey, encryptedInternalPassword) + if err = localStorage.Set(passwordKey, encryptedInternalPassword); err != nil { + return nil, + errors.Wrapf(err, "localStorage: failed to set %q", passwordKey) + } return internalPassword, nil } @@ -218,18 +236,18 @@ func initInternalPassword(externalPassword string, // getInternalPassword retrieves the internal password from local storage, // decrypts it, and returns it. func getInternalPassword( - externalPassword string, localStorage *LocalStorage) ([]byte, error) { - encryptedInternalPassword, err := localStorage.GetItem(passwordKey) + externalPassword string, localStorage *storage.LocalStorage) ([]byte, error) { + encryptedInternalPassword, err := localStorage.Get(passwordKey) if err != nil { return nil, errors.WithMessage(err, getPasswordStorageErr) } - salt, err := localStorage.GetItem(saltKey) + salt, err := localStorage.Get(saltKey) if err != nil { return nil, errors.WithMessage(err, getSaltStorageErr) } - paramsData, err := localStorage.GetItem(argonParamsKey) + paramsData, err := localStorage.Get(argonParamsKey) if err != nil { return nil, errors.WithMessage(err, getParamsStorageErr) } diff --git a/storage/password_test.go b/storage/password_test.go index 24f1ed035197a1856c9f1750d2072609d9feb9c1..0e4c4c64105e93247cf24d1665360fdbd834a687 100644 --- a/storage/password_test.go +++ b/storage/password_test.go @@ -14,9 +14,11 @@ import ( "crypto/rand" "encoding/base64" "fmt" - "gitlab.com/xx_network/crypto/csprng" "strings" "testing" + + "gitlab.com/elixxir/wasm-utils/storage" + "gitlab.com/xx_network/crypto/csprng" ) // Tests that running getOrInit twice returns the same internal password both @@ -77,7 +79,7 @@ func Test_changeExternalPassword(t *testing.T) { // Tests that verifyPassword returns true for a valid password and false for an // invalid password func Test_verifyPassword(t *testing.T) { - GetLocalStorage().Clear() + storage.GetLocalStorage().Clear() externalPassword := "myPassword" if _, err := getOrInit(externalPassword); err != nil { @@ -97,7 +99,7 @@ func Test_verifyPassword(t *testing.T) { // the encrypted one saved to local storage. func Test_initInternalPassword(t *testing.T) { externalPassword := "myPassword" - ls := GetLocalStorage() + ls := storage.GetLocalStorage() rng := csprng.NewSystemRNG() internalPassword, err := initInternalPassword( @@ -107,14 +109,14 @@ func Test_initInternalPassword(t *testing.T) { } // Attempt to retrieve encrypted internal password from storage - encryptedInternalPassword, err := ls.GetItem(passwordKey) + encryptedInternalPassword, err := ls.Get(passwordKey) if err != nil { t.Errorf( "Failed to load encrypted internal password from storage: %+v", err) } // Attempt to retrieve salt from storage - salt, err := ls.GetItem(saltKey) + salt, err := ls.Get(saltKey) if err != nil { t.Errorf("Failed to load salt from storage: %+v", err) } @@ -138,7 +140,7 @@ func Test_initInternalPassword(t *testing.T) { // error when read. func Test_initInternalPassword_CsprngReadError(t *testing.T) { externalPassword := "myPassword" - ls := GetLocalStorage() + ls := storage.GetLocalStorage() b := bytes.NewBuffer([]byte{}) expectedErr := strings.Split(readInternalPasswordErr, "%")[0] @@ -154,7 +156,7 @@ func Test_initInternalPassword_CsprngReadError(t *testing.T) { // return enough bytes. func Test_initInternalPassword_CsprngReadNumBytesError(t *testing.T) { externalPassword := "myPassword" - ls := GetLocalStorage() + ls := storage.GetLocalStorage() b := bytes.NewBuffer(make([]byte, internalPasswordLen/2)) expectedErr := fmt.Sprintf( @@ -171,7 +173,7 @@ func Test_initInternalPassword_CsprngReadNumBytesError(t *testing.T) { // to local storage by initInternalPassword. func Test_getInternalPassword(t *testing.T) { externalPassword := "myPassword" - ls := GetLocalStorage() + ls := storage.GetLocalStorage() rng := csprng.NewSystemRNG() internalPassword, err := initInternalPassword( @@ -196,7 +198,7 @@ func Test_getInternalPassword(t *testing.T) { // loaded from local storage. func Test_getInternalPassword_LocalStorageGetPasswordError(t *testing.T) { externalPassword := "myPassword" - ls := GetLocalStorage() + ls := storage.GetLocalStorage() ls.Clear() expectedErr := strings.Split(getPasswordStorageErr, "%")[0] @@ -212,9 +214,11 @@ func Test_getInternalPassword_LocalStorageGetPasswordError(t *testing.T) { // loaded from local storage. func Test_getInternalPassword_LocalStorageGetError(t *testing.T) { externalPassword := "myPassword" - ls := GetLocalStorage() + ls := storage.GetLocalStorage() ls.Clear() - ls.SetItem(passwordKey, []byte("password")) + if err := ls.Set(passwordKey, []byte("password")); err != nil { + t.Fatalf("Failed to set %q: %+v", passwordKey, err) + } expectedErr := strings.Split(getSaltStorageErr, "%")[0] @@ -229,11 +233,17 @@ func Test_getInternalPassword_LocalStorageGetError(t *testing.T) { // decrypted. func Test_getInternalPassword_DecryptPasswordError(t *testing.T) { externalPassword := "myPassword" - ls := GetLocalStorage() + ls := storage.GetLocalStorage() ls.Clear() - ls.SetItem(saltKey, []byte("salt")) - ls.SetItem(passwordKey, []byte("password")) - ls.SetItem(argonParamsKey, []byte(`{"Time": 1, "Memory": 65536, "Threads": 4}`)) + if err := ls.Set(saltKey, []byte("salt")); err != nil { + t.Errorf("failed to set %q: %+v", saltKey, err) + } + if err := ls.Set(passwordKey, []byte("password")); err != nil { + t.Errorf("failed to set %q: %+v", passwordKey, err) + } + if err := ls.Set(argonParamsKey, []byte(`{"Time": 1, "Memory": 65536, "Threads": 4}`)); err != nil { + t.Errorf("failed to set %q: %+v", argonParamsKey, err) + } expectedErr := strings.Split(decryptPasswordErr, "%")[0] diff --git a/storage/purge.go b/storage/purge.go index b80481b470863c32dee6cad894858cb081a08abe..df75cebf9175251d53c722565e7c1111d6d9262f 100644 --- a/storage/purge.go +++ b/storage/purge.go @@ -10,13 +10,15 @@ package storage import ( + "sync/atomic" + "syscall/js" + "github.com/hack-pad/go-indexeddb/idb" - "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/storage/utility" - "gitlab.com/elixxir/xxdk-wasm/utils" - "sync/atomic" - "syscall/js" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/storage" ) // numClientsRunning is an atomic that tracks the current number of Cmix @@ -49,7 +51,7 @@ func DecrementNumClientsRunning() { // passed into [wasm.NewCmix]. // // Returns: -// - Throws a TypeError if the password is incorrect or if not all cMix +// - Throws an error if the password is incorrect or if not all cMix // followers have been stopped. func Purge(_ js.Value, args []js.Value) any { storageDirectory := args[0].String() @@ -57,22 +59,21 @@ func Purge(_ js.Value, args []js.Value) any { // Check the password if !verifyPassword(userPassword) { - utils.Throw(utils.TypeError, errors.New("invalid password")) + exception.Throwf("invalid password") return nil } // Verify all Cmix followers are stopped if n := atomic.LoadUint64(&numClientsRunning); n != 0 { - utils.Throw(utils.TypeError, errors.Errorf( - "%d cMix followers running; all need to be stopped", n)) + exception.Throwf("%d cMix followers running; all need to be stopped", n) return nil } // Get all indexedDb database names databaseList, err := GetIndexedDbList() if err != nil { - utils.Throw(utils.TypeError, errors.Errorf( - "failed to get list of indexedDb database names: %+v", err)) + exception.Throwf( + "failed to get list of indexedDb database names: %+v", err) return nil } jww.DEBUG.Printf("[PURGE] Found %d databases to delete: %s", @@ -82,26 +83,34 @@ func Purge(_ js.Value, args []js.Value) any { for dbName := range databaseList { _, err = idb.Global().DeleteDatabase(dbName) if err != nil { - utils.Throw(utils.TypeError, errors.Errorf( - "failed to delete indexedDb database %q: %+v", dbName, err)) + exception.Throwf( + "failed to delete indexedDb database %q: %+v", dbName, err) return nil } } // Get local storage - ls := GetLocalStorage() + ls := storage.GetLocalStorage() // Clear all local storage saved by this WASM project - n := ls.ClearWASM() + n := ls.Clear() jww.DEBUG.Printf("[PURGE] Cleared %d WASM keys in local storage", n) // Clear all EKV from local storage - n = ls.ClearPrefix(storageDirectory) + keys := ls.LocalStorageUNSAFE().KeysPrefix(storageDirectory) + n = len(keys) + for _, keyName := range keys { + ls.LocalStorageUNSAFE().RemoveItem(keyName) + } jww.DEBUG.Printf("[PURGE] Cleared %d keys with the prefix %q (for EKV)", n, storageDirectory) // Clear all NDFs saved to local storage - n = ls.ClearPrefix(utility.NdfStorageKeyNamePrefix) + keys = ls.LocalStorageUNSAFE().KeysPrefix(utility.NdfStorageKeyNamePrefix) + n = len(keys) + for _, keyName := range keys { + ls.LocalStorageUNSAFE().RemoveItem(keyName) + } jww.DEBUG.Printf("[PURGE] Cleared %d keys with the prefix %q (for NDF)", n, utility.NdfStorageKeyNamePrefix) diff --git a/storage/version.go b/storage/version.go index b0f192ab24f8ad80d1fb9a9a5c96177f67545440..5fa7e439af29a96c66b22c92b86db42c384c121f 100644 --- a/storage/version.go +++ b/storage/version.go @@ -17,6 +17,7 @@ import ( jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/bindings" + "gitlab.com/elixxir/wasm-utils/storage" ) // SEMVER is the current semantic version of xxDK WASM. @@ -35,11 +36,11 @@ const ( // On first load, only the xxDK WASM and xxDK client versions are stored. func CheckAndStoreVersions() error { return checkAndStoreVersions( - SEMVER, bindings.GetVersion(), GetLocalStorage()) + SEMVER, bindings.GetVersion(), storage.GetLocalStorage()) } func checkAndStoreVersions( - currentWasmVer, currentClientVer string, ls *LocalStorage) error { + currentWasmVer, currentClientVer string, ls *storage.LocalStorage) error { // Get the stored client version, if it exists storedClientVer, err := initOrLoadStoredSemver(clientVerKey, currentClientVer, ls) @@ -76,8 +77,12 @@ func checkAndStoreVersions( // Upgrade path code goes here // Save current versions - ls.SetItem(clientVerKey, []byte(currentClientVer)) - ls.SetItem(semverKey, []byte(currentWasmVer)) + if err = ls.Set(clientVerKey, []byte(currentClientVer)); err != nil { + return errors.Wrapf(err, "localStorage: failed to set %q", clientVerKey) + } + if err = ls.Set(semverKey, []byte(currentWasmVer)); err != nil { + return errors.Wrapf(err, "localStorage: failed to set %q", semverKey) + } return nil } @@ -86,13 +91,16 @@ func checkAndStoreVersions( // local storage. If no version is stored, then the current version is stored // and returned. func initOrLoadStoredSemver( - key, currentVersion string, ls *LocalStorage) (string, error) { - storedVersion, err := ls.GetItem(key) + key, currentVersion string, ls *storage.LocalStorage) (string, error) { + storedVersion, err := ls.Get(key) if err != nil { if errors.Is(err, os.ErrNotExist) { // Save the current version if this is the first run jww.INFO.Printf("Initialising %s to v%s", key, currentVersion) - ls.SetItem(key, []byte(currentVersion)) + if err = ls.Set(key, []byte(currentVersion)); err != nil { + return "", + errors.Wrapf(err, "localStorage: failed to set %q", key) + } return currentVersion, nil } else { // If the item exists, but cannot be loaded, return an error diff --git a/storage/version_test.go b/storage/version_test.go index a8ead72e52a53d9840936a7deb6865d9aa30f201..27043680fea08beb01c6584557335a308d4c457a 100644 --- a/storage/version_test.go +++ b/storage/version_test.go @@ -11,12 +11,14 @@ package storage import ( "testing" + + "gitlab.com/elixxir/wasm-utils/storage" ) // Tests that checkAndStoreVersions correct initialises the client and WASM // versions on first run and upgrades them correctly on subsequent runs. func Test_checkAndStoreVersions(t *testing.T) { - ls := GetLocalStorage() + ls := storage.GetLocalStorage() ls.Clear() oldWasmVer := "0.1" newWasmVer := "1.0" @@ -28,7 +30,7 @@ func Test_checkAndStoreVersions(t *testing.T) { } // Check client version - storedClientVer, err := ls.GetItem(clientVerKey) + storedClientVer, err := ls.Get(clientVerKey) if err != nil { t.Errorf("Failed to get client version from storage: %+v", err) } @@ -38,7 +40,7 @@ func Test_checkAndStoreVersions(t *testing.T) { } // Check WASM version - storedWasmVer, err := ls.GetItem(semverKey) + storedWasmVer, err := ls.Get(semverKey) if err != nil { t.Errorf("Failed to get WASM version from storage: %+v", err) } @@ -53,7 +55,7 @@ func Test_checkAndStoreVersions(t *testing.T) { } // Check client version - storedClientVer, err = ls.GetItem(clientVerKey) + storedClientVer, err = ls.Get(clientVerKey) if err != nil { t.Errorf("Failed to get client version from storage: %+v", err) } @@ -63,7 +65,7 @@ func Test_checkAndStoreVersions(t *testing.T) { } // Check WASM version - storedWasmVer, err = ls.GetItem(semverKey) + storedWasmVer, err = ls.Get(semverKey) if err != nil { t.Errorf("Failed to get WASM version from storage: %+v", err) } @@ -76,7 +78,7 @@ func Test_checkAndStoreVersions(t *testing.T) { // Tests that initOrLoadStoredSemver initialises the correct version on first // run and returns the same version on subsequent runs. func Test_initOrLoadStoredSemver(t *testing.T) { - ls := GetLocalStorage() + ls := storage.GetLocalStorage() key := "testKey" oldVersion := "0.1" diff --git a/test/wasm_exec.js b/test/wasm_exec.js index c613dfc656f2799b6b3c922f59003cd3347454a7..07649aee63e71d945a3b558a249faa7240be4416 100644 --- a/test/wasm_exec.js +++ b/test/wasm_exec.js @@ -503,7 +503,7 @@ }, // func throw(exception string, message string) - 'gitlab.com/elixxir/xxdk-wasm/utils.throw': (sp) => { + 'gitlab.com/elixxir/wasm-utils/utils.throw': (sp) => { const exception = loadString(sp + 8) const message = loadString(sp + 24) throw globalThis[exception](message) diff --git a/utils/array.go b/utils/array.go deleted file mode 100644 index 3597d0ba9cca229e1141c164d9e1204cf56c117a..0000000000000000000000000000000000000000 --- a/utils/array.go +++ /dev/null @@ -1,71 +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 utils - -import ( - "bytes" - "encoding/base64" - "syscall/js" -) - -// Uint8ArrayToBase64 encodes an uint8 array to a base 64 string. -// -// Parameters: -// - args[0] - Javascript 8-bit unsigned integer array (Uint8Array). -// -// Returns: -// - Base 64 encoded string (string). -func Uint8ArrayToBase64(_ js.Value, args []js.Value) any { - return base64.StdEncoding.EncodeToString(CopyBytesToGo(args[0])) -} - -// Base64ToUint8Array decodes a base 64 encoded string to a Uint8Array. -// -// Parameters: -// - args[0] - Base 64 encoded string (string). -// -// Returns: -// - Javascript 8-bit unsigned integer array (Uint8Array). -// - Throws TypeError if decoding the string fails. -func Base64ToUint8Array(_ js.Value, args []js.Value) any { - b, err := base64ToUint8Array(args[0]) - if err != nil { - Throw(TypeError, err) - } - - return b -} - -// base64ToUint8Array is a helper function that returns an error instead of -// throwing it. -func base64ToUint8Array(base64String js.Value) (js.Value, error) { - b, err := base64.StdEncoding.DecodeString(base64String.String()) - if err != nil { - return js.Value{}, err - } - - return CopyBytesToJS(b), nil -} - -// Uint8ArrayEquals returns true if the two Uint8Array are equal and false -// otherwise. -// -// Parameters: -// - args[0] - Array A (Uint8Array). -// - args[1] - Array B (Uint8Array). -// -// Returns: -// - If the two arrays are equal (boolean). -func Uint8ArrayEquals(_ js.Value, args []js.Value) any { - a := CopyBytesToGo(args[0]) - b := CopyBytesToGo(args[1]) - - return bytes.Equal(a, b) -} diff --git a/utils/array_test.go b/utils/array_test.go deleted file mode 100644 index 0353d1cce9155cf47e4c8e21132809b6d7d1915c..0000000000000000000000000000000000000000 --- a/utils/array_test.go +++ /dev/null @@ -1,114 +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 utils - -import ( - "encoding/base64" - "fmt" - "strings" - "syscall/js" - "testing" -) - -var testBytes = [][]byte{ - nil, - {}, - {0}, - {0, 1, 2, 3}, - {214, 108, 207, 78, 229, 11, 42, 219, 42, 87, 205, 104, 252, 73, 223, - 229, 145, 209, 79, 111, 34, 96, 238, 127, 11, 105, 114, 62, 239, - 130, 145, 82, 3}, -} - -// Tests that a series of Uint8Array Javascript objects are correctly converted -// to base 64 strings with Uint8ArrayToBase64. -func TestUint8ArrayToBase64(t *testing.T) { - for i, val := range testBytes { - // Create Uint8Array and set each element individually - jsBytes := Uint8Array.New(len(val)) - for j, v := range val { - jsBytes.SetIndex(j, v) - } - - jsB64 := Uint8ArrayToBase64(js.Value{}, []js.Value{jsBytes}) - - expected := base64.StdEncoding.EncodeToString(val) - - if expected != jsB64 { - t.Errorf("Did not receive expected base64 encoded string (%d)."+ - "\nexpected: %s\nreceived: %s", i, expected, jsB64) - } - } -} - -// Tests that Base64ToUint8Array correctly decodes a series of base 64 encoded -// strings into Uint8Array. -func TestBase64ToUint8Array(t *testing.T) { - for i, val := range testBytes { - b64 := base64.StdEncoding.EncodeToString(val) - jsArr, err := base64ToUint8Array(js.ValueOf(b64)) - if err != nil { - t.Errorf("Failed to convert js.Value to base 64: %+v", err) - } - - // Generate the expected string to match the output of toString() on a - // Uint8Array - expected := strings.ReplaceAll(fmt.Sprintf("%d", val), " ", ",")[1:] - expected = expected[:len(expected)-1] - - // Get the string value of the Uint8Array - jsString := jsArr.Call("toString").String() - - if expected != jsString { - t.Errorf("Failed to recevie expected string representation of "+ - "the Uint8Array (%d).\nexpected: %s\nreceived: %s", - i, expected, jsString) - } - } -} - -// Tests that a base 64 encoded string decoded to Uint8Array via -// Base64ToUint8Array and back to a base 64 encoded string via -// Uint8ArrayToBase64 matches the original. -func TestBase64ToUint8ArrayUint8ArrayToBase64(t *testing.T) { - for i, val := range testBytes { - b64 := base64.StdEncoding.EncodeToString(val) - jsArr, err := base64ToUint8Array(js.ValueOf(b64)) - if err != nil { - t.Errorf("Failed to convert js.Value to base 64: %+v", err) - } - - jsB64 := Uint8ArrayToBase64(js.Value{}, []js.Value{jsArr}) - - if b64 != jsB64 { - t.Errorf("JSON from Uint8Array does not match original (%d)."+ - "\nexpected: %s\nreceived: %s", i, b64, jsB64) - } - } -} - -func TestUint8ArrayEquals(t *testing.T) { - for i, val := range testBytes { - // Create Uint8Array and set each element individually - jsBytesA := Uint8Array.New(len(val)) - for j, v := range val { - jsBytesA.SetIndex(j, v) - } - - jsBytesB := CopyBytesToJS(val) - - if !Uint8ArrayEquals(js.Value{}, []js.Value{jsBytesA, jsBytesB}).(bool) { - t.Errorf("Two equal byte slices were found to be different (%d)."+ - "\nexpected: %s\nreceived: %s", i, - jsBytesA.Call("toString").String(), - jsBytesB.Call("toString").String()) - } - } -} diff --git a/utils/convert.go b/utils/convert.go deleted file mode 100644 index b1f2cd10172bca7e88ff6c77931c754ab1b7f1d8..0000000000000000000000000000000000000000 --- a/utils/convert.go +++ /dev/null @@ -1,62 +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 utils - -import ( - "encoding/json" - "syscall/js" -) - -// CopyBytesToGo copies the [Uint8Array] stored in the [js.Value] to []byte. -// This is a wrapper for [js.CopyBytesToGo] to make it more convenient. -func CopyBytesToGo(src js.Value) []byte { - b := make([]byte, src.Length()) - js.CopyBytesToGo(b, src) - return b -} - -// CopyBytesToJS copies the []byte to a [Uint8Array] stored in a [js.Value]. -// This is a wrapper for [js.CopyBytesToJS] to make it more convenient. -func CopyBytesToJS(src []byte) js.Value { - dst := Uint8Array.New(len(src)) - js.CopyBytesToJS(dst, src) - return dst -} - -// JsToJson converts the Javascript value to JSON. -func JsToJson(value js.Value) string { - if value.IsUndefined() { - return "null" - } - - return JSON.Call("stringify", value).String() -} - -// JsonToJS converts a JSON bytes input to a [js.Value] of the object subtype. -func JsonToJS(inputJson []byte) (js.Value, error) { - var jsObj map[string]any - err := json.Unmarshal(inputJson, &jsObj) - if err != nil { - return js.ValueOf(nil), err - } - - return js.ValueOf(jsObj), nil -} - -// JsErrorToJson converts the Javascript error to JSON. This should be used for -// all Javascript error objects instead of JsonToJS. -func JsErrorToJson(value js.Value) string { - if value.IsUndefined() { - return "null" - } - - properties := Object.Call("getOwnPropertyNames", value) - return JSON.Call("stringify", value, properties).String() -} diff --git a/utils/convert_test.go b/utils/convert_test.go deleted file mode 100644 index 508c27f783f1365f28b4b7b78b7389fc4d3ff1a6..0000000000000000000000000000000000000000 --- a/utils/convert_test.go +++ /dev/null @@ -1,305 +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 utils - -import ( - "encoding/base64" - "encoding/json" - "sort" - "syscall/js" - "testing" -) - -import ( - "bytes" - "fmt" - "strings" -) - -// Tests that CopyBytesToGo returns a byte slice that matches the Uint8Array. -func TestCopyBytesToGo(t *testing.T) { - for i, val := range testBytes { - // Create Uint8Array and set each element individually - jsBytes := Uint8Array.New(len(val)) - for j, v := range val { - jsBytes.SetIndex(j, v) - } - - goBytes := CopyBytesToGo(jsBytes) - - if !bytes.Equal(val, goBytes) { - t.Errorf("Failed to recevie expected bytes from Uint8Array (%d)."+ - "\nexpected: %d\nreceived: %d", - i, val, goBytes) - } - } -} - -// Tests that CopyBytesToJS returns a Javascript Uint8Array with values matching -// the original byte slice. -func TestCopyBytesToJS(t *testing.T) { - for i, val := range testBytes { - jsBytes := CopyBytesToJS(val) - - // Generate the expected string to match the output of toString() on a - // Uint8Array - expected := strings.ReplaceAll(fmt.Sprintf("%d", val), " ", ",")[1:] - expected = expected[:len(expected)-1] - - // Get the string value of the Uint8Array - jsString := jsBytes.Call("toString").String() - - if expected != jsString { - t.Errorf("Failed to recevie expected string representation of "+ - "the Uint8Array (%d).\nexpected: %s\nreceived: %s", - i, expected, jsString) - } - } -} - -// Tests that a byte slice converted to Javascript via CopyBytesToJS and -// converted back to Go via CopyBytesToGo matches the original. -func TestCopyBytesToJSCopyBytesToGo(t *testing.T) { - for i, val := range testBytes { - jsBytes := CopyBytesToJS(val) - goBytes := CopyBytesToGo(jsBytes) - - if !bytes.Equal(val, goBytes) { - t.Errorf("Failed to recevie expected bytes from Uint8Array (%d)."+ - "\nexpected: %d\nreceived: %d", - i, val, goBytes) - } - } - -} - -// Tests that JsToJson can convert a Javascript object to JSON that matches the -// output of json.Marshal on the Go version of the same object. -func TestJsToJson(t *testing.T) { - testObj := map[string]any{ - "nil": nil, - "bool": true, - "int": 1, - "float": 1.5, - "string": "I am string", - "array": []any{1, 2, 3}, - "object": map[string]any{"int": 5}, - } - - expected, err := json.Marshal(testObj) - if err != nil { - t.Errorf("Failed to JSON marshal test object: %+v", err) - } - - jsJson := JsToJson(js.ValueOf(testObj)) - - // Javascript does not return the JSON object fields sorted so the letters - // of each Javascript string are sorted and compared - er := []rune(string(expected)) - sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] }) - jj := []rune(jsJson) - sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] }) - - if string(er) != string(jj) { - t.Errorf("Recieved incorrect JSON from Javascript object."+ - "\nexpected: %s\nreceived: %s", expected, jsJson) - } -} - -// Tests that JsToJson return a null object when the Javascript object is -// undefined. -func TestJsToJson_Undefined(t *testing.T) { - expected, err := json.Marshal(nil) - if err != nil { - t.Errorf("Failed to JSON marshal test object: %+v", err) - } - - jsJson := JsToJson(js.Undefined()) - - if string(expected) != jsJson { - t.Errorf("Recieved incorrect JSON from Javascript object."+ - "\nexpected: %s\nreceived: %s", expected, jsJson) - } -} - -// Tests that JsonToJS can convert a JSON object with multiple types to a -// Javascript object and that all values match. -func TestJsonToJS(t *testing.T) { - testObj := map[string]any{ - "nil": nil, - "bool": true, - "int": 1, - "float": 1.5, - "string": "I am string", - "bytes": []byte{1, 2, 3}, - "array": []any{1, 2, 3}, - "object": map[string]any{"int": 5}, - } - jsonData, err := json.Marshal(testObj) - if err != nil { - t.Errorf("Failed to JSON marshal test object: %+v", err) - } - - jsObj, err := JsonToJS(jsonData) - if err != nil { - t.Errorf("Failed to convert JSON to Javascript object: %+v", err) - } - - for key, val := range testObj { - jsVal := jsObj.Get(key) - switch key { - case "nil": - if !jsVal.IsNull() { - t.Errorf("Key %s is not null.", key) - } - case "bool": - if jsVal.Bool() != val { - t.Errorf("Incorrect value for key %s."+ - "\nexpected: %t\nreceived: %t", key, val, jsVal.Bool()) - } - case "int": - if jsVal.Int() != val { - t.Errorf("Incorrect value for key %s."+ - "\nexpected: %d\nreceived: %d", key, val, jsVal.Int()) - } - case "float": - if jsVal.Float() != val { - t.Errorf("Incorrect value for key %s."+ - "\nexpected: %f\nreceived: %f", key, val, jsVal.Float()) - } - case "string": - if jsVal.String() != val { - t.Errorf("Incorrect value for key %s."+ - "\nexpected: %s\nreceived: %s", key, val, jsVal.String()) - } - case "bytes": - if jsVal.String() != base64.StdEncoding.EncodeToString(val.([]byte)) { - t.Errorf("Incorrect value for key %s."+ - "\nexpected: %s\nreceived: %s", key, - base64.StdEncoding.EncodeToString(val.([]byte)), - jsVal.String()) - } - case "array": - for i, v := range val.([]any) { - if jsVal.Index(i).Int() != v { - t.Errorf("Incorrect value for key %s index %d."+ - "\nexpected: %d\nreceived: %d", - key, i, v, jsVal.Index(i).Int()) - } - } - case "object": - if jsVal.Get("int").Int() != val.(map[string]any)["int"] { - t.Errorf("Incorrect value for key %s."+ - "\nexpected: %d\nreceived: %d", key, - val.(map[string]any)["int"], jsVal.Get("int").Int()) - } - } - } -} - -// Tests that JSON can be converted to a Javascript object via JsonToJS and back -// to JSON using JsToJson and matches the original. -func TestJsonToJSJsToJson(t *testing.T) { - testObj := map[string]any{ - "nil": nil, - "bool": true, - "int": 1, - "float": 1.5, - "string": "I am string", - "bytes": []byte{1, 2, 3}, - "array": []any{1, 2, 3}, - "object": map[string]any{"int": 5}, - } - jsonData, err := json.Marshal(testObj) - if err != nil { - t.Errorf("Failed to JSON marshal test object: %+v", err) - } - - jsObj, err := JsonToJS(jsonData) - if err != nil { - t.Errorf("Failed to convert the Javascript object to JSON: %+v", err) - } - - jsJson := JsToJson(jsObj) - - // Javascript does not return the JSON object fields sorted so the letters - // of each Javascript string are sorted and compared - er := []rune(string(jsonData)) - sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] }) - jj := []rune(jsJson) - sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] }) - - if string(er) != string(jj) { - t.Errorf("JSON from Javascript does not match original."+ - "\nexpected: %s\nreceived: %s", jsonData, jsJson) - } -} - -// Tests that JsErrorToJson can convert a Javascript object to JSON that matches -// the output of json.Marshal on the Go version of the same object. -func TestJsErrorToJson(t *testing.T) { - testObj := map[string]any{ - "nil": nil, - "bool": true, - "int": 1, - "float": 1.5, - "string": "I am string", - "array": []any{1, 2, 3}, - "object": map[string]any{"int": 5}, - } - - expected, err := json.Marshal(testObj) - if err != nil { - t.Errorf("Failed to JSON marshal test object: %+v", err) - } - - jsJson := JsErrorToJson(js.ValueOf(testObj)) - - // Javascript does not return the JSON object fields sorted so the letters - // of each Javascript string are sorted and compared - er := []rune(string(expected)) - sort.SliceStable(er, func(i, j int) bool { return er[i] < er[j] }) - jj := []rune(jsJson) - sort.SliceStable(jj, func(i, j int) bool { return jj[i] < jj[j] }) - - if string(er) != string(jj) { - t.Errorf("Recieved incorrect JSON from Javascript object."+ - "\nexpected: %s\nreceived: %s", expected, jsJson) - } -} - -// Tests that JsErrorToJson return a null object when the Javascript object is -// undefined. -func TestJsErrorToJson_Undefined(t *testing.T) { - expected, err := json.Marshal(nil) - if err != nil { - t.Errorf("Failed to JSON marshal test object: %+v", err) - } - - jsJson := JsErrorToJson(js.Undefined()) - - if string(expected) != jsJson { - t.Errorf("Recieved incorrect JSON from Javascript object."+ - "\nexpected: %s\nreceived: %s", expected, jsJson) - } -} - -// Tests that JsErrorToJson returns a JSON object containing the original error -// string. -func TestJsErrorToJson_ErrorObject(t *testing.T) { - expected := "An error" - jsErr := Error.New(expected) - jsJson := JsErrorToJson(jsErr) - - if !strings.Contains(jsJson, expected) { - t.Errorf("Recieved incorrect JSON from Javascript error."+ - "\nexpected: %s\nreceived: %s", expected, jsJson) - } -} diff --git a/utils/errors.go b/utils/errors.go deleted file mode 100644 index 2e1cbacbce3b7c70a67f52e1217a76ef63f887b5..0000000000000000000000000000000000000000 --- a/utils/errors.go +++ /dev/null @@ -1,71 +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 utils - -import ( - "fmt" - "syscall/js" -) - -// JsError converts the error to a Javascript Error. -func JsError(err error) js.Value { - return Error.New(err.Error()) -} - -// JsTrace converts the error to a Javascript Error that includes the error's -// stack trace. -func JsTrace(err error) js.Value { - return Error.New(fmt.Sprintf("%+v", err)) -} - -// Throw function stub to throws Javascript exceptions. The exception must be -// one of the defined Exception below. Any other error types will result in an -// error. -func Throw(exception Exception, err error) { - throw(exception, fmt.Sprintf("%+v", err)) -} - -func throw(exception Exception, message string) - -// Exception are the possible Javascript error types that can be thrown. -type Exception string - -const ( - // EvalError occurs when error has occurred in the eval() function. - // - // Deprecated: This exception is not thrown by JavaScript anymore, however - // the EvalError object remains for compatibility. - EvalError Exception = "EvalError" - - // RangeError occurs when a numeric variable or parameter is outside its - // valid range. - RangeError Exception = "RangeError" - - // ReferenceError occurs when a variable that does not exist (or hasn't yet - // been initialized) in the current scope is referenced. - ReferenceError Exception = "ReferenceError" - - // SyntaxError occurs when trying to interpret syntactically invalid code. - SyntaxError Exception = "SyntaxError" - - // TypeError occurs when an operation could not be performed, typically (but - // not exclusively) when a value is not of the expected type. - // - // A TypeError may be thrown when: - // - an operand or argument passed to a function is incompatible with the - // type expected by that operator or function; or - // - when attempting to modify a value that cannot be changed; or - // - when attempting to use a value in an inappropriate way. - TypeError Exception = "TypeError" - - // URIError occurs when a global URI handling function was used in a wrong - // way. - URIError Exception = "URIError" -) diff --git a/utils/errors_test.go b/utils/errors_test.go deleted file mode 100644 index f9965e34878f0cd507e2839482e1f38201a99055..0000000000000000000000000000000000000000 --- a/utils/errors_test.go +++ /dev/null @@ -1,42 +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 utils - -import ( - "fmt" - "github.com/pkg/errors" - "testing" -) - -// Tests that TestJsError returns a Javascript Error object with the expected -// message. -func TestJsError(t *testing.T) { - err := errors.New("test error") - expectedErr := err.Error() - jsError := JsError(err).Get("message").String() - - if jsError != expectedErr { - t.Errorf("Failed to get expected error message."+ - "\nexpected: %s\nreceived: %s", expectedErr, jsError) - } -} - -// Tests that TestJsTrace returns a Javascript Error object with the expected -// message and stack trace. -func TestJsTrace(t *testing.T) { - err := errors.New("test error") - expectedErr := fmt.Sprintf("%+v", err) - jsError := JsTrace(err).Get("message").String() - - if jsError != expectedErr { - t.Errorf("Failed to get expected error message."+ - "\nexpected: %s\nreceived: %s", expectedErr, jsError) - } -} diff --git a/utils/utils.go b/utils/utils.go deleted file mode 100644 index cb1e46f3859695e7c77fbf0c9403eb24b7b10ff4..0000000000000000000000000000000000000000 --- a/utils/utils.go +++ /dev/null @@ -1,106 +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 utils - -import ( - "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - "syscall/js" -) - -var ( - // Error is the Javascript Error type. It used to create new Javascript - // errors. - Error = js.Global().Get("Error") - - // JSON is the Javascript JSON type. It is used to perform JSON operations - // on the Javascript layer. - JSON = js.Global().Get("JSON") - - // Object is the Javascript Object type. It is used to perform Object - // operations on the Javascript layer. - Object = js.Global().Get("Object") - - // Promise is the Javascript Promise type. It is used to generate new - // promises. - Promise = js.Global().Get("Promise") - - // Uint8Array is the Javascript Uint8Array type. It is used to create new - // Uint8Array. - Uint8Array = js.Global().Get("Uint8Array") -) - -// WrapCB wraps a Javascript function in an object so that it can be called -// later with only the arguments and without specifying the function name. -// -// Panics if m is not a function. -func WrapCB(parent js.Value, m string) func(args ...any) js.Value { - if parent.Get(m).Type() != js.TypeFunction { - // Create the error separate from the print so stack trace is printed - err := errors.Errorf("Function %q is not of type %s", m, js.TypeFunction) - jww.FATAL.Panicf("%+v", err) - } - - return func(args ...any) js.Value { return parent.Call(m, args...) } -} - -// PromiseFn converts the Javascript Promise construct into Go. -// -// Call resolve with the return of the function on success. Call reject with an -// error on failure. -type PromiseFn func(resolve, reject func(args ...any) js.Value) - -// CreatePromise creates a Javascript promise to return the value of a blocking -// Go function to Javascript. -func CreatePromise(f PromiseFn) any { - // Create handler for promise (this will be a Javascript function) - handler := js.FuncOf(func(this js.Value, args []js.Value) any { - // Spawn a new go routine to perform the blocking function - go func(resolve, reject js.Value) { - f(resolve.Invoke, reject.Invoke) - }(args[0], args[1]) - - return nil - }) - - // Create and return the Promise object - return Promise.New(handler) -} - -// Await waits on a Javascript value. It blocks until the awaitable successfully -// resolves to the result or rejects to err. -// -// If there is a result, err will be nil and vice versa. -func Await(awaitable js.Value) (result []js.Value, err []js.Value) { - then := make(chan []js.Value) - defer close(then) - thenFunc := js.FuncOf(func(this js.Value, args []js.Value) any { - then <- args - return nil - }) - defer thenFunc.Release() - - catch := make(chan []js.Value) - defer close(catch) - catchFunc := js.FuncOf(func(this js.Value, args []js.Value) any { - catch <- args - return nil - }) - defer catchFunc.Release() - - awaitable.Call("then", thenFunc).Call("catch", catchFunc) - - select { - case result = <-then: - return result, nil - case err = <-catch: - return nil, err - } -} diff --git a/utils/utils_js.s b/utils/utils_js.s deleted file mode 100644 index 45c1668a272247a134e6c85508bf5c09f0d7b3f0..0000000000000000000000000000000000000000 --- a/utils/utils_js.s +++ /dev/null @@ -1,6 +0,0 @@ -#include "textflag.h" - -// Throw enables throwing of Javascript exceptions. -TEXT ·throw(SB), NOSPLIT, $0 - CallImport - RET diff --git a/wasm/authenticatedConnection.go b/wasm/authenticatedConnection.go index 3308a9be32e8321c7d80c7fe5558c794407050b0..66162cfb68ec42906a3d596278cdf2b4801149b4 100644 --- a/wasm/authenticatedConnection.go +++ b/wasm/authenticatedConnection.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -73,7 +74,7 @@ func (ac *AuthenticatedConnection) SendE2E(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { sendReport, err := ac.api.SendE2E(mt, payload) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -86,7 +87,7 @@ func (ac *AuthenticatedConnection) SendE2E(_ js.Value, args []js.Value) any { // resources. // // Returns: -// - Throws a TypeError if closing fails. +// - Throws an error if closing fails. func (ac *AuthenticatedConnection) Close(js.Value, []js.Value) any { return ac.api.Close() } @@ -108,13 +109,13 @@ func (ac *AuthenticatedConnection) GetPartner(js.Value, []js.Value) any { // [bindings.Listener] interface. // // Returns: -// - Throws a TypeError is registering the listener fails. +// - Throws an error is registering the listener fails. func (ac *AuthenticatedConnection) RegisterListener( _ js.Value, args []js.Value) any { err := ac.api.RegisterListener(args[0].Int(), &listener{utils.WrapCB(args[1], "Hear"), utils.WrapCB(args[1], "Name")}) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -143,7 +144,7 @@ func (c *Cmix) ConnectWithAuthentication(_ js.Value, args []js.Value) any { ac, err := c.api.ConnectWithAuthentication( e2eID, recipientContact, e2eParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(newAuthenticatedConnectionJS(ac)) } diff --git a/wasm/backup.go b/wasm/backup.go index 9f5e7f6e7b302675ad8a385745dd645473961405..1a4a05de8d86410493544015501f852f72e7462d 100644 --- a/wasm/backup.go +++ b/wasm/backup.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -69,7 +70,7 @@ func (ubf *updateBackupFunc) UpdateBackup(encryptedBackup []byte) { // // Returns: // - JSON of [bindings.BackupReport] (Uint8Array). -// - Throws a TypeError if creating [Cmix] from backup fails. +// - Throws an error if creating [Cmix] from backup fails. func NewCmixFromBackup(_ js.Value, args []js.Value) any { ndfJSON := args[0].String() storageDir := args[1].String() @@ -80,7 +81,7 @@ func NewCmixFromBackup(_ js.Value, args []js.Value) any { report, err := bindings.NewCmixFromBackup(ndfJSON, storageDir, backupPassphrase, sessionPassword, backupFileContents) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -104,13 +105,13 @@ func NewCmixFromBackup(_ js.Value, args []js.Value) any { // // Returns: // - Javascript representation of the [Backup] object. -// - Throws a TypeError if initializing the [Backup] fails. +// - Throws an error if initializing the [Backup] fails. func InitializeBackup(_ js.Value, args []js.Value) any { cb := &updateBackupFunc{utils.WrapCB(args[3], "UpdateBackup")} api, err := bindings.InitializeBackup( args[0].Int(), args[1].Int(), args[2].String(), cb) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -133,12 +134,12 @@ func InitializeBackup(_ js.Value, args []js.Value) any { // // Returns: // - Javascript representation of the [Backup] object. -// - Throws a TypeError if initializing the [Backup] fails. +// - Throws an error if initializing the [Backup] fails. func ResumeBackup(_ js.Value, args []js.Value) any { cb := &updateBackupFunc{utils.WrapCB(args[2], "UpdateBackup")} api, err := bindings.ResumeBackup(args[0].Int(), args[1].Int(), cb) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -149,11 +150,11 @@ func ResumeBackup(_ js.Value, args []js.Value) any { // storage. To enable backups again, call [InitializeBackup]. // // Returns: -// - Throws a TypeError if stopping the backup fails. +// - Throws an error if stopping the backup fails. func (b *Backup) StopBackup(js.Value, []js.Value) any { err := b.api.StopBackup() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/channels.go b/wasm/channels.go index 4236d37e57f4dd0f6a84357dee8fd2a27594c2d7..653ff6b21506ea7a655d583dd6fd8e9be487502a 100644 --- a/wasm/channels.go +++ b/wasm/channels.go @@ -10,19 +10,17 @@ package wasm import ( - "crypto/ed25519" "encoding/base64" "encoding/json" "errors" - "gitlab.com/elixxir/client/v4/channels" - "gitlab.com/elixxir/crypto/message" - channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels" - "gitlab.com/xx_network/primitives/id" "sync" "syscall/js" "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/client/v4/channels" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" + channelsDb "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels" ) //////////////////////////////////////////////////////////////////////////////// @@ -60,7 +58,6 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any { "SendMessage": js.FuncOf(cm.SendMessage), "SendReply": js.FuncOf(cm.SendReply), "SendReaction": js.FuncOf(cm.SendReaction), - "SendInvite": js.FuncOf(cm.SendInvite), "DeleteMessage": js.FuncOf(cm.DeleteMessage), "PinMessage": js.FuncOf(cm.PinMessage), "MuteUser": js.FuncOf(cm.MuteUser), @@ -80,6 +77,11 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any { // Channel Receiving Logic and Callback Registration "RegisterReceiveHandler": js.FuncOf(cm.RegisterReceiveHandler), + + // Notifications + "SetMobileNotificationsLevel": js.FuncOf( + cm.SetMobileNotificationsLevel), + "GetNotificationLevel": js.FuncOf(cm.GetNotificationLevel), } return channelsManagerMap @@ -106,11 +108,11 @@ func (cm *ChannelsManager) GetID(js.Value, []js.Value) any { // // Returns: // - Marshalled bytes of [channel.PrivateIdentity] (Uint8Array). -// - Throws a TypeError if generating the identity fails. +// - Throws an error if generating the identity fails. func GenerateChannelIdentity(_ js.Value, args []js.Value) any { pi, err := bindings.GenerateChannelIdentity(args[0].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -129,7 +131,7 @@ var identityMap sync.Map // // Returns: // - JSON of [channel.Identity] (Uint8Array). -// - Throws a TypeError if constructing the identity fails. +// - Throws an error if constructing the identity fails. func ConstructIdentity(_ js.Value, args []js.Value) any { // Note: This function is similar to constructIdentity below except that it // uses a sync.Map backend to increase efficiency for identities that were @@ -145,7 +147,7 @@ func ConstructIdentity(_ js.Value, args []js.Value) any { identity, err := bindings.ConstructIdentity( utils.CopyBytesToGo(args[0]), args[1].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -163,12 +165,12 @@ func ConstructIdentity(_ js.Value, args []js.Value) any { // // Returns: // - JSON of [channel.Identity] (Uint8Array). -// - Throws a TypeError if constructing the identity fails. +// - Throws an error if constructing the identity fails. func constructIdentity(_ js.Value, args []js.Value) any { identity, err := bindings.ConstructIdentity( utils.CopyBytesToGo(args[0]), args[1].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -185,14 +187,14 @@ func constructIdentity(_ js.Value, args []js.Value) any { // // Returns: // - JSON of [channel.PrivateIdentity] (Uint8Array). -// - Throws a TypeError if importing the identity fails. +// - Throws an error if importing the identity fails. func ImportPrivateIdentity(_ js.Value, args []js.Value) any { password := args[0].String() data := utils.CopyBytesToGo(args[1]) pi, err := bindings.ImportPrivateIdentity(password, data) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -207,13 +209,13 @@ func ImportPrivateIdentity(_ js.Value, args []js.Value) any { // // Returns: // - JSON of the constructed [channel.Identity] (Uint8Array). -// - Throws a TypeError if unmarshalling the bytes or marshalling the identity +// - Throws an error if unmarshalling the bytes or marshalling the identity // fails. func GetPublicChannelIdentity(_ js.Value, args []js.Value) any { marshaledPublic := utils.CopyBytesToGo(args[0]) pi, err := bindings.GetPublicChannelIdentity(marshaledPublic) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -230,14 +232,14 @@ func GetPublicChannelIdentity(_ js.Value, args []js.Value) any { // // Returns: // - JSON of the public identity ([channel.Identity]) (Uint8Array). -// - Throws a TypeError if unmarshalling the bytes or marshalling the identity +// - Throws an error if unmarshalling the bytes or marshalling the identity // fails. func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any { marshaledPrivate := utils.CopyBytesToGo(args[0]) identity, err := bindings.GetPublicChannelIdentityFromPrivate(marshaledPrivate) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -257,27 +259,35 @@ func GetPublicChannelIdentityFromPrivate(_ js.Value, args []js.Value) any { // using [Cmix.GetID]. // - args[1] - Bytes of a private identity ([channel.PrivateIdentity]) that is // generated by [GenerateChannelIdentity] (Uint8Array). -// - args[2] - JSON of an array of integers of [channels.ExtensionBuilder] +// - args[2] - A function that initialises and returns a Javascript object +// that matches the [bindings.EventModel] interface. The function must match +// the Build function in [bindings.EventModelBuilder]. +// - args[3] - JSON of an array of integers of [channels.ExtensionBuilder] // IDs. The ID can be retrieved from an object with an extension builder // (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not // using extension builders. Example: `[2,11,5]` (Uint8Array). -// - args[3] - A function that initialises and returns a Javascript object -// that matches the [bindings.EventModel] interface. The function must match -// the Build function in [bindings.EventModelBuilder]. +// - args[4] - ID of [Notifications] object in tracker. This can be retrieved +// using [Notifications.GetID] (int). +// - args[5] - A Javascript object that implements the function on +// [bindings.ChannelUICallbacks]. It is a callback that informs the UI about +// various events. The entire interface can be nil, but if defined, each +// method must be implemented. // // Returns: // - Javascript representation of the [ChannelsManager] object. -// - Throws a TypeError if creating the manager fails. +// - Throws an error if creating the manager fails. func NewChannelsManager(_ js.Value, args []js.Value) any { cmixId := args[0].Int() privateIdentity := utils.CopyBytesToGo(args[1]) - extensionBuilderIDsJSON := utils.CopyBytesToGo(args[2]) - em := newEventModelBuilder(args[3]) + em := newEventModelBuilder(args[2]) + extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3]) + notificationsID := args[4].Int() + cUI := newChannelUI(args[5]) cm, err := bindings.NewChannelsManager( - cmixId, privateIdentity, extensionBuilderIDsJSON, em) + cmixId, privateIdentity, em, extensionBuilderIDsJSON, notificationsID, cUI) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -297,18 +307,34 @@ func NewChannelsManager(_ js.Value, args []js.Value) any { // using [Cmix.GetID]. // - args[1] - The storage tag associated with the previously created channel // manager and retrieved with [ChannelsManager.GetStorageTag] (string). -// - args[2] - A function that initialises and returns a Javascript object +// - args[2] - A function that initializes and returns a Javascript object // that matches the [bindings.EventModel] interface. The function must match // the Build function in [bindings.EventModelBuilder]. +// - args[3] - JSON of an array of integers of [channels.ExtensionBuilder] +// IDs. The ID can be retrieved from an object with an extension builder +// (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not +// using extension builders. Example: `[2,11,5]`. +// - args[4] - ID of [Notifications] object in tracker. This can be retrieved +// using [Notifications.GetID] (int). +// - args[5] - A Javascript object that implements the function on +// [bindings.ChannelUICallbacks]. It is a callback that informs the UI about +// various events. The entire interface can be nil, but if defined, each +// method must be implemented. // // Returns: // - Javascript representation of the [ChannelsManager] object. -// - Throws a TypeError if loading the manager fails. +// - Throws an error if loading the manager fails. func LoadChannelsManager(_ js.Value, args []js.Value) any { + cmixID := args[0].Int() + storageTag := args[1].String() em := newEventModelBuilder(args[2]) - cm, err := bindings.LoadChannelsManager(args[0].Int(), args[1].String(), em) + extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3]) + notificationsID := args[4].Int() + cUI := newChannelUI(args[5]) + cm, err := bindings.LoadChannelsManager( + cmixID, storageTag, em, extensionBuilderIDsJSON, notificationsID, cUI) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -336,48 +362,36 @@ func LoadChannelsManager(_ js.Value, args []js.Value) any { // IDs. The ID can be retrieved from an object with an extension builder // (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not // using extension builders. Example: `[2,11,5]` (Uint8Array). -// - args[4] - The received message callback, which is called everytime a -// message is added or changed in the database. It is a function that takes -// in the same parameters as [channels.MessageReceivedCallback]. On the -// Javascript side, the UUID is returned as an int and the channelID as a -// Uint8Array. The row in the database that was updated can be found using -// the UUID. The channel ID is provided so that the recipient can filter if -// they want to the processes the update now or not. An "update" bool is -// present which tells you if the row is new or if it is an edited old row. -// - args[5] - The deleted message callback, which is called everytime a -// message is deleted from the database. It is a function that takes in the -// same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript -// side, the message ID is returned as a Uint8Array. -// - args[6] - The muted user callback, which is called everytime a user is -// muted or unmuted. It is a function that takes in the same parameters as -// [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and -// user public key are returned as Uint8Array. -// - args[7] - ID of [ChannelDbCipher] object in tracker (int). Create this +// - args[4] - ID of [Notifications] object in tracker. This can be retrieved +// using [Notifications.GetID] (int). +// - args[5] - A Javascript object that implements the function on +// [bindings.ChannelUICallbacks]. It is a callback that informs the UI about +// various events. The entire interface can be nil, but if defined, each +// method must be implemented. +// - args[6] - ID of [ChannelDbCipher] object in tracker (int). Create this // object with [NewChannelsDatabaseCipher] and get its id with // [ChannelDbCipher.GetID]. // // Returns a promise: // - Resolves to a Javascript representation of the [ChannelsManager] object. // - Rejected with an error if loading indexedDb or the manager fails. -// - Throws a TypeError if the cipher ID does not correspond to a cipher. +// - Throws an error if the cipher ID does not correspond to a cipher. func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any { cmixID := args[0].Int() wasmJsPath := args[1].String() privateIdentity := utils.CopyBytesToGo(args[2]) extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3]) - messageReceivedCB := args[4] - deletedMessageCB := args[5] - mutedUserCB := args[6] - cipherID := args[7].Int() + notificationsID := args[4].Int() + cUI := newChannelUI(args[5]) + cipherID := args[6].Int() cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) } return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity, - extensionBuilderIDsJSON, messageReceivedCB, deletedMessageCB, - mutedUserCB, cipher) + extensionBuilderIDsJSON, notificationsID, cUI, cipher) } // NewChannelsManagerWithIndexedDbUnsafe creates a new [ChannelsManager] from a @@ -402,22 +416,12 @@ func NewChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any { // IDs. The ID can be retrieved from an object with an extension builder // (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not // using extension builders. Example: `[2,11,5]` (Uint8Array). -// - args[4] - The received message callback, which is called everytime a -// message is added or changed in the database. It is a function that takes -// in the same parameters as [indexedDb.MessageReceivedCallback]. On the -// Javascript side, the UUID is returned as an int and the channelID as a -// Uint8Array. The row in the database that was updated can be found using -// the UUID. The channel ID is provided so that the recipient can filter if -// they want to the processes the update now or not. An "update" bool is -// present which tells you if the row is new or if it is an edited old row. -// - args[5] - The deleted message callback, which is called everytime a -// message is deleted from the database. It is a function that takes in the -// same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript -// side, the message ID is returned as a Uint8Array. -// - args[6] - The muted user callback, which is called everytime a user is -// muted or unmuted. It is a function that takes in the same parameters as -// [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and -// user public key are returned as Uint8Array. +// - args[4] - ID of [Notifications] object in tracker. This can be retrieved +// using [Notifications.GetID] (int). +// - args[5] - A Javascript object that implements the function on +// [bindings.ChannelUICallbacks]. It is a callback that informs the UI about +// various events. The entire interface can be nil, but if defined, each +// method must be implemented. // // Returns a promise: // - Resolves to a Javascript representation of the [ChannelsManager] object. @@ -429,40 +433,27 @@ func NewChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any { wasmJsPath := args[1].String() privateIdentity := utils.CopyBytesToGo(args[2]) extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3]) - messageReceivedCB := args[4] - deletedMessageCB := args[5] - mutedUserCB := args[6] + notificationsID := args[4].Int() + cUI := newChannelUI(args[5]) return newChannelsManagerWithIndexedDb(cmixID, wasmJsPath, privateIdentity, - extensionBuilderIDsJSON, messageReceivedCB, deletedMessageCB, - mutedUserCB, nil) + extensionBuilderIDsJSON, notificationsID, cUI, nil) } func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string, - privateIdentity, extensionBuilderIDsJSON []byte, messageReceivedCB, - deletedMessageCB, mutedUserCB js.Value, cipher *bindings.ChannelDbCipher) any { - - messageReceived := func(uuid uint64, channelID *id.ID, update bool) { - messageReceivedCB.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update) - } - - deletedMessage := func(messageID message.ID) { - deletedMessageCB.Invoke(utils.CopyBytesToJS(messageID.Marshal())) - } - - mutedUser := func(channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) { - mutedUserCB.Invoke(utils.CopyBytesToJS(channelID.Marshal()), - utils.CopyBytesToJS(pubKey), unmute) - } + privateIdentity, extensionBuilderIDsJSON []byte, notificationsID int, + channelsCbs bindings.ChannelUICallbacks, + cipher *bindings.ChannelDbCipher) any { model := channelsDb.NewWASMEventModelBuilder( - wasmJsPath, cipher, messageReceived, deletedMessage, mutedUser) + wasmJsPath, cipher, channelsCbs) promiseFn := func(resolve, reject func(args ...any) js.Value) { - cm, err := bindings.NewChannelsManagerGoEventModel( - cmixID, privateIdentity, extensionBuilderIDsJSON, model) + cm, err := bindings.NewChannelsManagerGoEventModel(cmixID, + privateIdentity, extensionBuilderIDsJSON, model, notificationsID, + channelsCbs) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(newChannelsManagerJS(cm)) } @@ -485,22 +476,16 @@ func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string, // - args[1] - Path to Javascript file that starts the worker (string). // - args[2] - The storage tag associated with the previously created channel // manager and retrieved with [ChannelsManager.GetStorageTag] (string). -// - args[3] - The received message callback, which is called everytime a -// message is added or changed in the database. It is a function that takes -// in the same parameters as [indexedDb.MessageReceivedCallback]. On the -// Javascript side, the UUID is returned as an int and the channelID as a -// Uint8Array. The row in the database that was updated can be found using -// the UUID. The channel ID is provided so that the recipient can filter if -// they want to the processes the update now or not. An "update" bool is -// present which tells you if the row is new or if it is an edited old row. -// - args[4] - The deleted message callback, which is called everytime a -// message is deleted from the database. It is a function that takes in the -// same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript -// side, the message ID is returned as a Uint8Array. -// - args[5] - The muted user callback, which is called everytime a user is -// muted or unmuted. It is a function that takes in the same parameters as -// [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and -// user public key are returned as Uint8Array. +// - args[3] - JSON of an array of integers of [channels.ExtensionBuilder] +// IDs. The ID can be retrieved from an object with an extension builder +// (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not +// using extension builders. Example: `[2,11,5]` (Uint8Array). +// - args[4] - ID of [Notifications] object in tracker. This can be retrieved +// using [Notifications.GetID] (int). +// - args[5] - A Javascript object that implements the function on +// [bindings.ChannelUICallbacks]. It is a callback that informs the UI about +// various events. The entire interface can be nil, but if defined, each +// method must be implemented. // - args[6] - ID of [ChannelDbCipher] object in tracker (int). Create this // object with [NewChannelsDatabaseCipher] and get its id with // [ChannelDbCipher.GetID]. @@ -508,23 +493,23 @@ func newChannelsManagerWithIndexedDb(cmixID int, wasmJsPath string, // Returns a promise: // - Resolves to a Javascript representation of the [ChannelsManager] object. // - Rejected with an error if loading indexedDb or the manager fails. -// - Throws a TypeError if the cipher ID does not correspond to a cipher. +// - Throws an error if the cipher ID does not correspond to a cipher. func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any { cmixID := args[0].Int() wasmJsPath := args[1].String() storageTag := args[2].String() - messageReceivedCB := args[3] - deletedMessageCB := args[4] - mutedUserCB := args[5] + extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3]) + notificationsID := args[4].Int() + channelsCbs := newChannelUI(args[5]) cipherID := args[6].Int() cipher, err := bindings.GetChannelDbCipherTrackerFromID(cipherID) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) } return loadChannelsManagerWithIndexedDb(cmixID, wasmJsPath, storageTag, - messageReceivedCB, deletedMessageCB, mutedUserCB, cipher) + extensionBuilderIDsJSON, notificationsID, channelsCbs, cipher) } // LoadChannelsManagerWithIndexedDbUnsafe loads an existing [ChannelsManager] @@ -543,22 +528,16 @@ func LoadChannelsManagerWithIndexedDb(_ js.Value, args []js.Value) any { // - args[1] - Path to Javascript file that starts the worker (string). // - args[2] - The storage tag associated with the previously created channel // manager and retrieved with [ChannelsManager.GetStorageTag] (string). -// - args[3] - The received message callback, which is called everytime a -// message is added or changed in the database. It is a function that takes -// in the same parameters as [indexedDb.MessageReceivedCallback]. On the -// Javascript side, the UUID is returned as an int and the channelID as a -// Uint8Array. The row in the database that was updated can be found using -// the UUID. The channel ID is provided so that the recipient can filter if -// they want to the processes the update now or not. An "update" bool is -// present which tells you if the row is new or if it is an edited old row. -// - args[4] - The deleted message callback, which is called everytime a -// message is deleted from the database. It is a function that takes in the -// same parameters as [indexedDb.DeletedMessageCallback]. On the Javascript -// side, the message ID is returned as a Uint8Array. -// - args[5] - The muted user callback, which is called everytime a user is -// muted or unmuted. It is a function that takes in the same parameters as -// [indexedDb.MutedUserCallback]. On the Javascript side, the channel ID and -// user public key are returned as Uint8Array. +// - args[3] - JSON of an array of integers of [channels.ExtensionBuilder] +// IDs. The ID can be retrieved from an object with an extension builder +// (e.g., [ChannelsFileTransfer.GetExtensionBuilderID]). Leave empty if not +// using extension builders. Example: `[2,11,5]` (Uint8Array). +// - args[4] - ID of [Notifications] object in tracker. This can be retrieved +// using [Notifications.GetID] (int). +// - args[5] - A Javascript object that implements the function on +// [bindings.ChannelUICallbacks]. It is a callback that informs the UI about +// various events. The entire interface can be nil, but if defined, each +// method must be implemented. // // Returns a promise: // - Resolves to a Javascript representation of the [ChannelsManager] object. @@ -567,39 +546,27 @@ func LoadChannelsManagerWithIndexedDbUnsafe(_ js.Value, args []js.Value) any { cmixID := args[0].Int() wasmJsPath := args[1].String() storageTag := args[2].String() - messageReceivedCB := args[3] - deletedMessageCB := args[3] - mutedUserCB := args[4] + extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3]) + notificationsID := args[4].Int() + cUI := newChannelUI(args[5]) return loadChannelsManagerWithIndexedDb(cmixID, wasmJsPath, storageTag, - messageReceivedCB, deletedMessageCB, mutedUserCB, nil) + extensionBuilderIDsJSON, notificationsID, cUI, nil) } func loadChannelsManagerWithIndexedDb(cmixID int, wasmJsPath, storageTag string, - messageReceivedCB, deletedMessageCB, mutedUserCB js.Value, + extensionBuilderIDsJSON []byte, notificationsID int, channelsCbs bindings.ChannelUICallbacks, cipher *bindings.ChannelDbCipher) any { - messageReceived := func(uuid uint64, channelID *id.ID, update bool) { - messageReceivedCB.Invoke(uuid, utils.CopyBytesToJS(channelID.Marshal()), update) - } - - deletedMessage := func(messageID message.ID) { - deletedMessageCB.Invoke(utils.CopyBytesToJS(messageID.Marshal())) - } - - mutedUser := func(channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) { - mutedUserCB.Invoke(utils.CopyBytesToJS(channelID.Marshal()), - utils.CopyBytesToJS(pubKey), unmute) - } - model := channelsDb.NewWASMEventModelBuilder( - wasmJsPath, cipher, messageReceived, deletedMessage, mutedUser) + wasmJsPath, cipher, channelsCbs) promiseFn := func(resolve, reject func(args ...any) js.Value) { cm, err := bindings.LoadChannelsManagerGoEventModel( - cmixID, storageTag, model, nil) + cmixID, storageTag, model, extensionBuilderIDsJSON, notificationsID, + channelsCbs) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(newChannelsManagerJS(cm)) } @@ -625,7 +592,7 @@ func loadChannelsManagerWithIndexedDb(cmixID int, wasmJsPath, storageTag string, func DecodePublicURL(_ js.Value, args []js.Value) any { c, err := bindings.DecodePublicURL(args[0].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -647,7 +614,7 @@ func DecodePublicURL(_ js.Value, args []js.Value) any { func DecodePrivateURL(_ js.Value, args []js.Value) any { c, err := bindings.DecodePrivateURL(args[0].String(), args[1].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -678,7 +645,7 @@ func DecodePrivateURL(_ js.Value, args []js.Value) any { func GetChannelJSON(_ js.Value, args []js.Value) any { c, err := bindings.GetChannelJSON(args[0].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -697,11 +664,11 @@ func GetChannelJSON(_ js.Value, args []js.Value) any { // Returns: // - JSON of [bindings.ChannelInfo], which describes all relevant channel info // (Uint8Array). -// - Throws a TypeError if getting the channel info fails. +// - Throws an error if getting the channel info fails. func GetChannelInfo(_ js.Value, args []js.Value) any { ci, err := bindings.GetChannelInfo(args[0].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -749,7 +716,7 @@ func (cm *ChannelsManager) GenerateChannel(_ js.Value, args []js.Value) any { prettyPrint, err := cm.api.GenerateChannel(name, description, privacyLevel) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(prettyPrint) } @@ -779,7 +746,7 @@ func (cm *ChannelsManager) JoinChannel(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { ci, err := cm.api.JoinChannel(channelPretty) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(ci)) } @@ -803,7 +770,7 @@ func (cm *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { err := cm.api.LeaveChannel(marshalledChanId) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve() } @@ -822,13 +789,13 @@ func (cm *ChannelsManager) LeaveChannel(_ js.Value, args []js.Value) any { // - args[0] - Marshalled bytes of the channel's [id.ID] (Uint8Array). // // Returns: -// - Throws a TypeError if the replay fails. +// - Throws an error if the replay fails. func (cm *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any { marshalledChanId := utils.CopyBytesToGo(args[0]) err := cm.api.ReplayChannel(marshalledChanId) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -839,7 +806,7 @@ func (cm *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any { // // Returns: // - JSON of an array of marshalled [id.ID] (Uint8Array). -// - Throws a TypeError if getting the channels fails. +// - Throws an error if getting the channels fails. // // JSON Example: // @@ -850,7 +817,7 @@ func (cm *ChannelsManager) ReplayChannel(_ js.Value, args []js.Value) any { func (cm *ChannelsManager) GetChannels(js.Value, []js.Value) any { channelList, err := cm.api.GetChannels() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -864,12 +831,12 @@ func (cm *ChannelsManager) GetChannels(js.Value, []js.Value) any { // - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). // // Returns: -// - Throws a TypeError if saving the DM token fails. +// - Throws an error if saving the DM token fails. func (cm *ChannelsManager) EnableDirectMessages(_ js.Value, args []js.Value) any { marshalledChanId := utils.CopyBytesToGo(args[0]) err := cm.api.EnableDirectMessages(marshalledChanId) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return nil @@ -882,12 +849,12 @@ func (cm *ChannelsManager) EnableDirectMessages(_ js.Value, args []js.Value) any // - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). // // Returns: -// - Throws a TypeError if saving the DM token fails +// - Throws an error if saving the DM token fails func (cm *ChannelsManager) DisableDirectMessages(_ js.Value, args []js.Value) any { marshalledChanId := utils.CopyBytesToGo(args[0]) err := cm.api.DisableDirectMessages(marshalledChanId) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return nil @@ -900,12 +867,12 @@ func (cm *ChannelsManager) DisableDirectMessages(_ js.Value, args []js.Value) an // // Returns: // - enabled (bool) - status of dms for passed in channel ID, true if enabled -// - Throws a TypeError if unmarshalling the channel ID +// - Throws an error if unmarshalling the channel ID func (cm *ChannelsManager) AreDMsEnabled(_ js.Value, args []js.Value) any { marshalledChanId := utils.CopyBytesToGo(args[0]) enabled, err := cm.api.AreDMsEnabled(marshalledChanId) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return false } return enabled @@ -948,7 +915,7 @@ type ShareURL struct { // // Returns: // - JSON of [bindings.ShareURL] (Uint8Array). -// - Throws a TypeError if generating the URL fails. +// - Throws an error if generating the URL fails. func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any { cmixID := args[0].Int() host := args[1].String() @@ -957,7 +924,7 @@ func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any { su, err := cm.api.GetShareURL(cmixID, host, maxUses, marshalledChanId) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -973,7 +940,7 @@ func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any { // Returns: // - An int that corresponds to the [broadcast.PrivacyLevel] as outlined // below (int). -// - Throws a TypeError if parsing the URL fails. +// - Throws an error if parsing the URL fails. // // Possible returns: // @@ -983,7 +950,7 @@ func (cm *ChannelsManager) GetShareURL(_ js.Value, args []js.Value) any { func GetShareUrlType(_ js.Value, args []js.Value) any { level, err := bindings.GetShareUrlType(args[0].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1027,6 +994,16 @@ func ValidForever(js.Value, []js.Value) any { // to the user should be tracked while all actions should not be (boolean). // - args[5] - JSON of [xxdk.CMIXParams]. If left empty // [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// - args[6] - JSON of a slice of public keys of users that should receive +// mobile notifications for the message. +// +// Example slice of public keys: +// +// [ +// "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=", +// "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=", +// "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo=" +// ] // // Returns a promise: // - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). @@ -1038,13 +1015,13 @@ func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any { leaseTimeMS := int64(args[3].Int()) tracked := args[4].Bool() cmixParamsJSON := utils.CopyBytesToGo(args[5]) + pingsJSON := utils.CopyBytesToGo(args[6]) - // fixme: add pings to wasm promiseFn := func(resolve, reject func(args ...any) js.Value) { sendReport, err := cm.api.SendGeneric(marshalledChanId, messageType, - msg, leaseTimeMS, tracked, cmixParamsJSON, nil) + msg, leaseTimeMS, tracked, cmixParamsJSON, pingsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1072,6 +1049,16 @@ func (cm *ChannelsManager) SendGeneric(_ js.Value, args []js.Value) any { // be enumerated here. Use [ValidForever] to last the max message life. // - args[3] - JSON of [xxdk.CMIXParams]. If left empty // [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// - args[4] - JSON of a slice of public keys of users that should receive +// mobile notifications for the message. +// +// Example slice of public keys: +// +// [ +// "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=", +// "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=", +// "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo=" +// ] // // Returns a promise: // - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). @@ -1081,13 +1068,13 @@ func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any { msg := args[1].String() leaseTimeMS := int64(args[2].Int()) cmixParamsJSON := utils.CopyBytesToGo(args[3]) + pingsJSON := utils.CopyBytesToGo(args[4]) promiseFn := func(resolve, reject func(args ...any) js.Value) { - // fixme: add pings to wasm sendReport, err := cm.api.SendMessage( - marshalledChanId, msg, leaseTimeMS, cmixParamsJSON, nil) + marshalledChanId, msg, leaseTimeMS, cmixParamsJSON, pingsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1122,6 +1109,16 @@ func (cm *ChannelsManager) SendMessage(_ js.Value, args []js.Value) any { // be enumerated here. Use [ValidForever] to last the max message life. // - args[4] - JSON of [xxdk.CMIXParams]. If left empty // [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// - args[5] - JSON of a slice of public keys of users that should receive +// mobile notifications for the message. +// +// Example slice of public keys: +// +// [ +// "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=", +// "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=", +// "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo=" +// ] // // Returns a promise: // - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). @@ -1132,13 +1129,13 @@ func (cm *ChannelsManager) SendReply(_ js.Value, args []js.Value) any { messageToReactTo := utils.CopyBytesToGo(args[2]) leaseTimeMS := int64(args[3].Int()) cmixParamsJSON := utils.CopyBytesToGo(args[4]) + pingsJSON := utils.CopyBytesToGo(args[5]) promiseFn := func(resolve, reject func(args ...any) js.Value) { - // fixme: add pings to wasm sendReport, err := cm.api.SendReply(marshalledChanId, msg, - messageToReactTo, leaseTimeMS, cmixParamsJSON, nil) + messageToReactTo, leaseTimeMS, cmixParamsJSON, pingsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1186,7 +1183,46 @@ func (cm *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any { sendReport, err := cm.api.SendReaction(marshalledChanId, reaction, messageToReactTo, leaseTimeMS, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// SendSilent is used to send to a channel a message with no notifications. +// Its primary purpose is to communicate new nicknames without calling +// [SendMessage]. +// +// It takes no payload intentionally as the message should be very lightweight. +// +// Parameters: +// - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - args[1] - The lease of the message. This will be how long the +// message is available from the network, in milliseconds (int). As per the +// [channels.Manager] documentation, this has different meanings depending +// on the use case. These use cases may be generic enough that they will not +// be enumerated here. Use [ValidForever] to last the max message life. +// - args[2] - JSON of [xxdk.CMIXParams]. If left empty +// [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (cm *ChannelsManager) SendSilent(_ js.Value, args []js.Value) any { + var ( + marshalledChanId = utils.CopyBytesToGo(args[0]) + leaseTimeMS = int64(args[1].Int()) + cmixParamsJSON = utils.CopyBytesToGo(args[2]) + ) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := cm.api.SendSilent( + marshalledChanId, leaseTimeMS, cmixParamsJSON) + if err != nil { + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1215,6 +1251,16 @@ func (cm *ChannelsManager) SendReaction(_ js.Value, args []js.Value) any { // be enumerated here. Use [ValidForever] to last the max message life. // - args[6] - JSON of [xxdk.CMIXParams]. If left empty // [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// - args[7] - JSON of a slice of public keys of users that should receive +// mobile notifications for the message. +// +// Example slice of public keys: +// +// [ +// "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=", +// "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=", +// "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo=" +// ] // // Returns a promise: // - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). @@ -1228,15 +1274,15 @@ func (cm *ChannelsManager) SendInvite(_ js.Value, args []js.Value) any { maxUses = args[4].Int() leaseTimeMS = int64(args[5].Int()) cmixParamsJSON = utils.CopyBytesToGo(args[6]) + pingsJSON = utils.CopyBytesToGo(args[7]) ) - // fixme: add pings to wasm promiseFn := func(resolve, reject func(args ...any) js.Value) { sendReport, err := cm.api.SendInvite(marshalledChanId, marshalledInviteToId, msg, host, maxUses, leaseTimeMS, - cmixParamsJSON, nil) + cmixParamsJSON, pingsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1293,7 +1339,7 @@ func (cm *ChannelsManager) SendAdminGeneric(_ js.Value, args []js.Value) any { sendReport, err := cm.api.SendAdminGeneric(marshalledChanId, messageType, msg, leaseTimeMS, tracked, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1332,7 +1378,7 @@ func (cm *ChannelsManager) DeleteMessage(_ js.Value, args []js.Value) any { sendReport, err := cm.api.DeleteMessage( channelIdBytes, targetMessageIdBytes, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1374,7 +1420,7 @@ func (cm *ChannelsManager) PinMessage(_ js.Value, args []js.Value) any { sendReport, err := cm.api.PinMessage(channelIdBytes, targetMessageIdBytes, undoAction, validUntilMS, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1415,7 +1461,7 @@ func (cm *ChannelsManager) MuteUser(_ js.Value, args []js.Value) any { sendReport, err := cm.api.MuteUser(channelIdBytes, mutedUserPubKeyBytes, undoAction, validUntilMS, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1437,7 +1483,7 @@ func (cm *ChannelsManager) MuteUser(_ js.Value, args []js.Value) any { func (cm *ChannelsManager) GetIdentity(js.Value, []js.Value) any { i, err := cm.api.GetIdentity() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1456,7 +1502,7 @@ func (cm *ChannelsManager) GetIdentity(js.Value, []js.Value) any { func (cm *ChannelsManager) ExportPrivateIdentity(_ js.Value, args []js.Value) any { i, err := cm.api.ExportPrivateIdentity(args[0].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1485,7 +1531,7 @@ func (cm *ChannelsManager) GetStorageTag(js.Value, []js.Value) any { func (cm *ChannelsManager) SetNickname(_ js.Value, args []js.Value) any { err := cm.api.SetNickname(args[0].String(), utils.CopyBytesToGo(args[1])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1503,7 +1549,7 @@ func (cm *ChannelsManager) SetNickname(_ js.Value, args []js.Value) any { func (cm *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) any { err := cm.api.DeleteNickname(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1522,7 +1568,7 @@ func (cm *ChannelsManager) DeleteNickname(_ js.Value, args []js.Value) any { func (cm *ChannelsManager) GetNickname(_ js.Value, args []js.Value) any { nickname, err := cm.api.GetNickname(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1544,7 +1590,7 @@ func (cm *ChannelsManager) GetNickname(_ js.Value, args []js.Value) any { func IsNicknameValid(_ js.Value, args []js.Value) any { err := bindings.IsNicknameValid(args[0].String()) if err != nil { - return utils.JsError(err) + return exception.NewError(err) } return nil @@ -1558,13 +1604,13 @@ func IsNicknameValid(_ js.Value, args []js.Value) any { // Returns: // - Returns true if the user is muted in the channel and false otherwise // (boolean). -// - Throws a TypeError if the channel ID cannot be unmarshalled. +// - Throws an error if the channel ID cannot be unmarshalled. func (cm *ChannelsManager) Muted(_ js.Value, args []js.Value) any { channelIDBytes := utils.CopyBytesToGo(args[0]) muted, err := cm.api.Muted(channelIDBytes) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1581,7 +1627,7 @@ func (cm *ChannelsManager) Muted(_ js.Value, args []js.Value) any { // Returns: // - JSON of an array of ed25519.PublicKey (Uint8Array). Look below for an // example. -// - Throws a TypeError if the channel ID cannot be unmarshalled. +// - Throws an error if the channel ID cannot be unmarshalled. // // Example return: // @@ -1590,13 +1636,151 @@ func (cm *ChannelsManager) GetMutedUsers(_ js.Value, args []js.Value) any { channelIDBytes := utils.CopyBytesToGo(args[0]) mutedUsers, err := cm.api.GetMutedUsers(channelIDBytes) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return utils.CopyBytesToJS(mutedUsers) } +//////////////////////////////////////////////////////////////////////////////// +// Notifications // +//////////////////////////////////////////////////////////////////////////////// + +// GetNotificationLevel returns the [channels.NotificationLevel] for the given +// channel. +// +// Parameters: +// - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array). +// +// Returns: +// - The [channels.NotificationLevel] for the channel (int). +// - Throws an error if the channel ID cannot be unmarshalled or the channel +// cannot be found. +func (cm *ChannelsManager) GetNotificationLevel(_ js.Value, args []js.Value) any { + channelIDBytes := utils.CopyBytesToGo(args[0]) + + level, err := cm.api.GetNotificationLevel(channelIDBytes) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + return level +} + +// SetMobileNotificationsLevel sets the notification level for the given +// channel. The [channels.NotificationLevel] dictates the type of notifications +// received and the status controls weather the notification is push or in-app. +// If muted, both the level and status must be set to mute. +// +// To use push notifications, a token must be registered with the notification +// manager. Note, when enabling push notifications, information may be shared +// with third parties (i.e., Firebase and Google's Palantir) and may represent a +// security risk to the user. +// +// Parameters: +// - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array). +// - args[1] - The [channels.NotificationLevel] to set for the channel (int). +// - args[2] - The [notifications.NotificationState] to set for the channel +// (int). +// +// Returns: +// - Throws an error if setting the notification level fails. +func (cm *ChannelsManager) SetMobileNotificationsLevel(_ js.Value, args []js.Value) any { + channelIDBytes := utils.CopyBytesToGo(args[0]) + level := args[1].Int() + status := args[2].Int() + + err := cm.api.SetMobileNotificationsLevel(channelIDBytes, level, status) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + return nil +} + +// GetNotificationReportsForMe checks the notification data against the filter +// list to determine which notifications belong to the user. A list of +// notification reports is returned detailing all notifications for the user. +// +// Parameters: +// - notificationFilterJSON - JSON of a slice of [channels.NotificationFilter]. +// - notificationDataJSON - JSON of a slice of [notifications.Data]. +// +// Example JSON of a slice of [channels.NotificationFilter]: +// [ +// +// { +// "identifier": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUDYXN5bUlkZW50aWZpZXI=", +// "channelID": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUD", +// "tags": ["6de69009a93d53793ee344e8fb48fae194eaf51861d3cc51c7348c337d13aedf-usrping"], +// "allowLists": { +// "allowWithTags": {}, +// "allowWithoutTags": {"102":{}, "2":{}} +// } +// }, +// { +// "identifier": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUDc3ltSWRlbnRpZmllcg==", +// "channelID": "O8NUg0KaDo18ybTKajXM/sgqEYS37+lewPhGV/2sMAUD", +// "tags": ["6de69009a93d53793ee344e8fb48fae194eaf51861d3cc51c7348c337d13aedf-usrping"], +// "allowLists": { +// "allowWithTags": {}, +// "allowWithoutTags": {"1":{}, "40000":{}} +// } +// }, +// { +// "identifier": "jCRgFRQvzzKOb8DJ0fqCRLgr9kiHN9LpqHXVhyHhhlQDYXN5bUlkZW50aWZpZXI=", +// "channelID": "jCRgFRQvzzKOb8DJ0fqCRLgr9kiHN9LpqHXVhyHhhlQD", +// "tags": ["6de69009a93d53793ee344e8fb48fae194eaf51861d3cc51c7348c337d13aedf-usrping"], +// "allowLists": { +// "allowWithTags": {}, +// "allowWithoutTags": {"102":{}, "2":{}} +// } +// } +// ] +// +// Example JSON of a slice of [notifications.Data]: +// +// [ +// { +// "EphemeralID": -6475, +// "RoundID": 875, +// "IdentityFP": "jWG/UuxRjD80HEo0WX3KYIag5LCfgaWKAg==", +// "MessageHash": "hDGE46QWa3d70y5nJTLbEaVmrFJHOyp2" +// }, +// { +// "EphemeralID": -2563, +// "RoundID": 875, +// "IdentityFP": "gL4nhCGKPNBm6YZ7KC0v4JThw65N9bRLTQ==", +// "MessageHash": "WcS4vGrSWDK8Kj7JYOkMo8kSh1Xti94V" +// }, +// { +// "EphemeralID": -13247, +// "RoundID": 875, +// "IdentityFP": "qV3uD++VWPhD2rRMmvrP9j8hp+jpFSsUHg==", +// "MessageHash": "VX6Tw7N48j7U2rRXYle20mFZi0If4CB1" +// } +// ] +// +// Returns: +// - The JSON of a slice of [channels.NotificationReport] (Uint8Array). +// - Throws an error if getting the report fails. +func GetNotificationReportsForMe(_ js.Value, args []js.Value) any { + notificationFilterJSON := utils.CopyBytesToGo(args[0]) + notificationDataJSON := utils.CopyBytesToGo(args[1]) + + report, err := bindings.GetNotificationReportsForMe( + notificationFilterJSON, notificationDataJSON) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + return utils.CopyBytesToJS(report) +} + //////////////////////////////////////////////////////////////////////////////// // Admin Management // //////////////////////////////////////////////////////////////////////////////// @@ -1609,11 +1793,11 @@ func (cm *ChannelsManager) GetMutedUsers(_ js.Value, args []js.Value) any { // Returns: // - True if the user is an admin in the channel and false otherwise // (boolean). -// - Throws a TypeError if the channel ID cannot be unmarshalled. +// - Throws an error if the channel ID cannot be unmarshalled. func (cm *ChannelsManager) IsChannelAdmin(_ js.Value, args []js.Value) any { isAdmin, err := cm.api.IsChannelAdmin(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1645,12 +1829,12 @@ func (cm *ChannelsManager) IsChannelAdmin(_ js.Value, args []js.Value) any { // Returns: // - Portable string of the channel private key encrypted with the password // (Uint8Array). -// - Throws a TypeError if the user is not an admin for the channel. +// - Throws an error if the user is not an admin for the channel. func (cm *ChannelsManager) ExportChannelAdminKey(_ js.Value, args []js.Value) any { pk, err := cm.api.ExportChannelAdminKey( utils.CopyBytesToGo(args[0]), args[1].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return utils.CopyBytesToJS(pk) @@ -1668,14 +1852,14 @@ func (cm *ChannelsManager) ExportChannelAdminKey(_ js.Value, args []js.Value) an // Returns: // - Returns false if private key does not belong to the given channel ID // (boolean). -// - Throws a TypeError if the password is invalid. +// - Throws an error if the password is invalid. // // Returns: // - bool - True if the private key belongs to the channel and false // otherwise. -// - Throws a TypeError with the message [channels.WrongPasswordErr] for an +// - Throws an error with the message [channels.WrongPasswordErr] for an // invalid password. -// - Throws a TypeError with the message [channels.ChannelDoesNotExistsErr] i +// - Throws an error with the message [channels.ChannelDoesNotExistsErr] i // the channel has not already been joined. func (cm *ChannelsManager) VerifyChannelAdminKey(_ js.Value, args []js.Value) any { channelID := utils.CopyBytesToGo(args[0]) @@ -1684,7 +1868,7 @@ func (cm *ChannelsManager) VerifyChannelAdminKey(_ js.Value, args []js.Value) an valid, err := cm.api.VerifyChannelAdminKey( channelID, encryptionPassword, encryptedPrivKey) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1702,13 +1886,13 @@ func (cm *ChannelsManager) VerifyChannelAdminKey(_ js.Value, args []js.Value) an // - args[2] - The encrypted channel private key packet (Uint8Array). // // Returns: -// - Throws a TypeError if the password is invalid or the private key does +// - Throws an error if the password is invalid or the private key does // not match the channel ID. -// - Throws a TypeError with the message [channels.WrongPasswordErr] for an +// - Throws an error with the message [channels.WrongPasswordErr] for an // invalid password. -// - Throws a TypeError with the message [channels.ChannelDoesNotExistsErr] if +// - Throws an error with the message [channels.ChannelDoesNotExistsErr] if // the channel has not already been joined. -// - Throws a TypeError with the message [channels.WrongPrivateKeyErr] if the +// - Throws an error with the message [channels.WrongPrivateKeyErr] if the // private key does not belong to the channel. func (cm *ChannelsManager) ImportChannelAdminKey(_ js.Value, args []js.Value) any { channelID := utils.CopyBytesToGo(args[0]) @@ -1717,7 +1901,7 @@ func (cm *ChannelsManager) ImportChannelAdminKey(_ js.Value, args []js.Value) an err := cm.api.ImportChannelAdminKey( channelID, encryptionPassword, encryptedPrivKey) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1734,11 +1918,11 @@ func (cm *ChannelsManager) ImportChannelAdminKey(_ js.Value, args []js.Value) an // - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array) // // Returns: -// - Throws a TypeError if the deletion fails. +// - Throws an error if the deletion fails. func (cm *ChannelsManager) DeleteChannelAdminKey(_ js.Value, args []js.Value) any { err := cm.api.DeleteChannelAdminKey(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1768,7 +1952,7 @@ type channelMessageReceptionCallback struct { func (cmrCB *channelMessageReceptionCallback) Callback( receivedChannelMessageReport []byte, err error) int { uuid := cmrCB.callback( - utils.CopyBytesToJS(receivedChannelMessageReport), utils.JsTrace(err)) + utils.CopyBytesToJS(receivedChannelMessageReport), exception.NewTrace(err)) return uuid.Int() } @@ -1796,7 +1980,7 @@ func (cmrCB *channelMessageReceptionCallback) Callback( // users (boolean). // // Returns: -// - Throws a TypeError if registering the handler fails. +// - Throws an error if registering the handler fails. func (cm *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) any { messageType := args[0].Int() listenerCb := &channelMessageReceptionCallback{ @@ -1809,7 +1993,7 @@ func (cm *ChannelsManager) RegisterReceiveHandler(_ js.Value, args []js.Value) a err := cm.api.RegisterReceiveHandler( messageType, listenerCb, name, userSpace, adminSpace, mutedSpace) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1841,7 +2025,7 @@ func GetNoMessageErr(js.Value, []js.Value) any { // Returns // - True if the error contains channels.NoMessageErr (boolean). func CheckNoMessageErr(_ js.Value, args []js.Value) any { - return bindings.CheckNoMessageErr(utils.JsErrorToJson(args[0])) + return bindings.CheckNoMessageErr(js.Error{Value: args[0]}.Error()) } // eventModelBuilder adheres to the [bindings.EventModelBuilder] interface. @@ -2227,7 +2411,7 @@ func newChannelDbCipherJS(api *bindings.ChannelDbCipher) map[string]any { // // Returns: // - JavaScript representation of the [ChannelDbCipher] object. -// - Throws a TypeError if creating the cipher fails. +// - Throws an error if creating the cipher fails. func NewChannelsDatabaseCipher(_ js.Value, args []js.Value) any { cmixId := args[0].Int() password := utils.CopyBytesToGo(args[1]) @@ -2236,7 +2420,7 @@ func NewChannelsDatabaseCipher(_ js.Value, args []js.Value) any { cipher, err := bindings.NewChannelsDatabaseCipher( cmixId, password, plaintTextBlockSize) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -2262,11 +2446,11 @@ func (c *ChannelDbCipher) GetID(js.Value, []js.Value) any { // // Returns: // - The ciphertext of the plaintext passed in (Uint8Array). -// - Throws a TypeError if it fails to encrypt the plaintext. +// - Throws an error if it fails to encrypt the plaintext. func (c *ChannelDbCipher) Encrypt(_ js.Value, args []js.Value) any { ciphertext, err := c.api.Encrypt(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -2283,11 +2467,11 @@ func (c *ChannelDbCipher) Encrypt(_ js.Value, args []js.Value) any { // // Returns: // - The plaintext of the ciphertext passed in (Uint8Array). -// - Throws a TypeError if it fails to encrypt the plaintext. +// - Throws an error if it fails to encrypt the plaintext. func (c *ChannelDbCipher) Decrypt(_ js.Value, args []js.Value) any { plaintext, err := c.api.Decrypt(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -2298,11 +2482,11 @@ func (c *ChannelDbCipher) Decrypt(_ js.Value, args []js.Value) any { // // Returns: // - JSON of the cipher (Uint8Array). -// - Throws a TypeError if marshalling fails. +// - Throws an error if marshalling fails. func (c *ChannelDbCipher) MarshalJSON(js.Value, []js.Value) any { data, err := c.api.MarshalJSON() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -2319,12 +2503,32 @@ func (c *ChannelDbCipher) MarshalJSON(js.Value, []js.Value) any { // // Returns: // - JSON of the cipher (Uint8Array). -// - Throws a TypeError if marshalling fails. +// - Throws an error if marshalling fails. func (c *ChannelDbCipher) UnmarshalJSON(_ js.Value, args []js.Value) any { err := c.api.UnmarshalJSON(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return nil } + +// newChannelUI maps the methods on the Javascript object to the +// channelUI callbacks implementation struct. +func newChannelUI(cbImpl js.Value) *channelUI { + return &channelUI{ + eventUpdate: utils.WrapCB(cbImpl, "EventUpdate"), + } +} + +// eventModel wraps Javascript callbacks to adhere to the +// [bindings.ChannelUICallbacks] interface. +type channelUI struct { + eventUpdate func(args ...any) js.Value +} + +// EventUpdate implements +// [bindings.ChannelUICallbacks.EventUpdate]. +func (c *channelUI) EventUpdate(eventType int64, jsonData []byte) { + c.eventUpdate(int(eventType), utils.CopyBytesToJS(jsonData)) +} diff --git a/wasm/channelsFileTransfer.go b/wasm/channelsFileTransfer.go index 5155e4ad4668bef0f3ad52f52efec748e7dbb0ff..2a76200423e1ac2aefb94a90857e8c70dc7777c1 100644 --- a/wasm/channelsFileTransfer.go +++ b/wasm/channelsFileTransfer.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -67,7 +68,7 @@ func InitChannelsFileTransfer(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { cft, err := bindings.InitChannelsFileTransfer(e2eID, paramsJson) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(newChannelsFileTransferJS(cft)) } @@ -164,7 +165,7 @@ func (cft *ChannelsFileTransfer) Upload(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { fileID, err := cft.api.Upload(fileData, retry, progressCB, period) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(fileID)) } @@ -211,7 +212,7 @@ func (cft *ChannelsFileTransfer) Send(_ js.Value, args []js.Value) any { fileID, err := cft.api.Send(channelIdBytes, fileLinkJSON, fileName, fileType, preview, validUntilMS, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(fileID)) } @@ -264,7 +265,7 @@ func (cft *ChannelsFileTransfer) RegisterSentProgressCallback( err := cft.api.RegisterSentProgressCallback( fileIDBytes, progressCB, periodMS) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve() } @@ -305,7 +306,7 @@ func (cft *ChannelsFileTransfer) RetryUpload(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { err := cft.api.RetryUpload(fileIDBytes, progressCB, periodMS) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve() } @@ -334,7 +335,7 @@ func (cft *ChannelsFileTransfer) CloseSend(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { err := cft.api.CloseSend(fileIDBytes) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve() } @@ -386,7 +387,7 @@ func (cft *ChannelsFileTransfer) Download(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { fileID, err := cft.api.Download(fileInfoJSON, progressCB, periodMS) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(fileID)) } @@ -438,7 +439,7 @@ func (cft *ChannelsFileTransfer) RegisterReceivedProgressCallback( err := cft.api.RegisterReceivedProgressCallback( fileIDBytes, progressCB, periodMS) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve() } @@ -490,7 +491,7 @@ type ftSentCallback struct { func (fsc *ftSentCallback) Callback( payload []byte, t *bindings.ChFilePartTracker, err error) { fsc.callback(utils.CopyBytesToJS(payload), newChFilePartTrackerJS(t), - utils.JsTrace(err)) + exception.NewTrace(err)) } // ftReceivedCallback wraps Javascript callbacks to adhere to the @@ -518,7 +519,7 @@ type ftReceivedCallback struct { func (frc *ftReceivedCallback) Callback( payload []byte, t *bindings.ChFilePartTracker, err error) { frc.callback(utils.CopyBytesToJS(payload), newChFilePartTrackerJS(t), - utils.JsTrace(err)) + exception.NewTrace(err)) } //////////////////////////////////////////////////////////////////////////////// diff --git a/wasm/channels_test.go b/wasm/channels_test.go index 028e6cbf305e46bc87f398abca1a93918e1f3669..1b4d9eb555177951ea66e9fb81ed7420418f2b22 100644 --- a/wasm/channels_test.go +++ b/wasm/channels_test.go @@ -12,7 +12,7 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/crypto/channel" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/utils" "gitlab.com/xx_network/crypto/csprng" "reflect" "syscall/js" diff --git a/wasm/cmix.go b/wasm/cmix.go index 88c9853db7654fb065ab91a6fa9b0dc1fcbddd54..be7ad6d31f2052b6f9750703611b4789cff35009 100644 --- a/wasm/cmix.go +++ b/wasm/cmix.go @@ -10,9 +10,11 @@ package wasm import ( - "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" "syscall/js" + + "gitlab.com/elixxir/client/v4/bindings" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" ) // Cmix wraps the [bindings.Cmix] object so its methods can be wrapped to be @@ -27,7 +29,11 @@ func newCmixJS(api *bindings.Cmix) map[string]any { c := Cmix{api} cmix := map[string]any{ // cmix.go - "GetID": js.FuncOf(c.GetID), + "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), // identity.go "MakeReceptionIdentity": js.FuncOf( @@ -95,7 +101,7 @@ func NewCmix(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { err := bindings.NewCmix(ndfJSON, storageDir, password, registrationCode) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve() } @@ -130,7 +136,38 @@ func LoadCmix(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { net, err := bindings.LoadCmix(storageDir, password, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) + } else { + resolve(newCmixJS(net)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// LoadSynchronizedCmix 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 LoadSynchronizedCmix(_ 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(exception.NewTrace(err)) } else { resolve(newCmixJS(net)) } @@ -146,3 +183,73 @@ func LoadCmix(_ js.Value, args []js.Value) any { func (c *Cmix) GetID(js.Value, []js.Value) any { return c.api.GetID() } + +// GetReceptionID returns the default reception identity for this cMix instance. +// +// Returns: +// - Marshalled bytes of [id.ID] (Uint8Array). +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: +// - args[0] - Key (string). +// +// Returns a promise: +// - Resolves to the value (Uint8Array) +// - Rejected with an error if accessing the KV fails. +func (c *Cmix) EKVGet(_ js.Value, args []js.Value) any { + key := args[0].String() + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + val, err := c.api.EKVGet(key) + if err != nil { + reject(exception.NewTrace(err)) + } else { + resolve(utils.CopyBytesToJS(val)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// EKVSet sets a value inside the secure encrypted key value store. +// +// Parameters: +// - args[0] - Key (string). +// - args[1] - Value (Uint8Array). +// +// Returns a promise: +// - Resolves on a successful save (void). +// - Rejected with an error if saving fails. +func (c *Cmix) EKVSet(_ js.Value, args []js.Value) any { + key := args[0].String() + val := utils.CopyBytesToGo(args[1]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := c.api.EKVSet(key, val) + if err != nil { + reject(exception.NewTrace(err)) + } else { + resolve(nil) + } + } + + return utils.CreatePromise(promiseFn) +} diff --git a/wasm/collective.go b/wasm/collective.go new file mode 100644 index 0000000000000000000000000000000000000000..e99f03aefab09cde52cde0107193033e6bb3ae16 --- /dev/null +++ b/wasm/collective.go @@ -0,0 +1,586 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/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), + "HasPrefix": js.FuncOf(rkv.HasPrefix), + "Prefix": js.FuncOf(rkv.Prefix), + "Root": js.FuncOf(rkv.Root), + "IsMemStore": js.FuncOf(rkv.IsMemStore), + "GetFullKey": js.FuncOf(rkv.GetFullKey), + "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(exception.NewTrace(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(exception.NewTrace(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(exception.NewTrace(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(exception.NewTrace(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(exception.NewTrace(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) +} + +// 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(exception.NewTrace(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(exception.NewTrace(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(exception.NewTrace(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(exception.NewTrace(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(exception.NewTrace(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(exception.NewTrace(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(exception.NewTrace(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 := exception.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 := exception.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 := exception.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 := exception.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 := exception.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/collective_test.go b/wasm/collective_test.go new file mode 100644 index 0000000000000000000000000000000000000000..8c512adce32127a2a448882167799e6268454fea --- /dev/null +++ b/wasm/collective_test.go @@ -0,0 +1,57 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 ( + "reflect" + "testing" + + "gitlab.com/elixxir/client/v4/bindings" +) + +// Tests that the map representing RemoteKV returned by newRemoteKvJS contains +// all of the methods on RemoteKV. +func Test_newRemoteKvJS(t *testing.T) { + rkvType := reflect.TypeOf(&RemoteKV{}) + + rkv := newRemoteKvJS(&bindings.RemoteKV{}) + if len(rkv) != rkvType.NumMethod() { + t.Errorf("RemoteKV JS object does not have all methods."+ + "\nexpected: %d\nreceived: %d", rkvType.NumMethod(), len(rkv)) + } + + for i := 0; i < rkvType.NumMethod(); i++ { + method := rkvType.Method(i) + + if _, exists := rkv[method.Name]; !exists { + t.Errorf("Method %s does not exist.", method.Name) + } + } +} + +// Tests that RemoteKV has all the methods that [bindings.RemoteKV] has. +func Test_RemoteKVMethods(t *testing.T) { + rkvType := reflect.TypeOf(&RemoteKV{}) + binRkvType := reflect.TypeOf(&bindings.RemoteKV{}) + + if binRkvType.NumMethod() != rkvType.NumMethod() { + t.Errorf("WASM RemoteKV object does not have all methods from "+ + "bindings.\nexpected: %d\nreceived: %d", + binRkvType.NumMethod(), rkvType.NumMethod()) + } + + for i := 0; i < binRkvType.NumMethod(); i++ { + method := binRkvType.Method(i) + + if _, exists := rkvType.MethodByName(method.Name); !exists { + t.Errorf("Method %s does not exist.", method.Name) + } + } +} diff --git a/wasm/connect.go b/wasm/connect.go index bfb95a6d882f808277a856dfbda9937c3bb3d687..b6b78e6940f1314d6d189e737d016db89112110b 100644 --- a/wasm/connect.go +++ b/wasm/connect.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -68,7 +69,7 @@ func (c *Cmix) Connect(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { api, err := c.api.Connect(e2eID, recipientContact, e2eParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(newConnectJS(api)) } @@ -95,7 +96,7 @@ func (c *Connection) SendE2E(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { sendReport, err := c.api.SendE2E(e2eID, payload) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -107,11 +108,11 @@ func (c *Connection) SendE2E(_ js.Value, args []js.Value) any { // Close deletes this [Connection]'s [partner.Manager] and releases resources. // // Returns: -// - Throws a TypeError if closing fails. +// - Throws an error if closing fails. func (c *Connection) Close(js.Value, []js.Value) any { err := c.api.Close() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -153,12 +154,12 @@ func (l *listener) Name() string { return l.name().String() } // [bindings.Listener] interface. // // Returns: -// - Throws a TypeError is registering the listener fails. +// - Throws an error is registering the listener fails. func (c *Connection) RegisterListener(_ js.Value, args []js.Value) any { err := c.api.RegisterListener(args[0].Int(), &listener{utils.WrapCB(args[1], "Hear"), utils.WrapCB(args[1], "Name")}) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/delivery.go b/wasm/delivery.go index 327f15223426bf3ac19c4d001806b8e69abdfb9e..67bc677bc300ca0d5108b33dd5573f1f0d80c4ed 100644 --- a/wasm/delivery.go +++ b/wasm/delivery.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -79,7 +80,7 @@ func (mdc *messageDeliveryCallback) EventCallback( // occurs, in milliseconds (int). // // Returns: -// - Throws a TypeError if the parameters are invalid or getting round results +// - Throws an error if the parameters are invalid or getting round results // fails. func (c *Cmix) WaitForRoundResult(_ js.Value, args []js.Value) any { roundList := utils.CopyBytesToGo(args[0]) @@ -87,7 +88,7 @@ func (c *Cmix) WaitForRoundResult(_ js.Value, args []js.Value) any { err := c.api.WaitForRoundResult(roundList, mdc, args[2].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/dm.go b/wasm/dm.go index 6b6ea90ab546580dbf0d6e312ab4211df4ff335f..7295c8d887c1cca03ee00ec0818a44ea622361de 100644 --- a/wasm/dm.go +++ b/wasm/dm.go @@ -20,8 +20,9 @@ import ( "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/client/v4/dm" "gitlab.com/elixxir/crypto/codename" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" indexDB "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/dm" - "gitlab.com/elixxir/xxdk-wasm/utils" ) //////////////////////////////////////////////////////////////////////////////// @@ -53,12 +54,20 @@ func newDMClientJS(api *bindings.DMClient) map[string]any { "GetBlockedSenders": js.FuncOf(cm.GetBlockedSenders), "GetDatabaseName": js.FuncOf(cm.GetDatabaseName), + // Share URL + "GetShareURL": js.FuncOf(cm.GetShareURL), + // DM Sending Methods and Reports "SendText": js.FuncOf(cm.SendText), "SendReply": js.FuncOf(cm.SendReply), "SendReaction": js.FuncOf(cm.SendReaction), "SendInvite": js.FuncOf(cm.SendInvite), + "SendSilent": js.FuncOf(cm.SendSilent), "Send": js.FuncOf(cm.Send), + + // User Mute/Unmute + "BlockSender": js.FuncOf(cm.BlockSender), + "UnblockSender": js.FuncOf(cm.UnblockSender), } return dmClientMap @@ -83,14 +92,14 @@ func newDMClientJS(api *bindings.DMClient) map[string]any { // // Returns: // - Javascript representation of the [DMClient] object. -// - Throws a TypeError if creating the manager fails. +// - Throws an error if creating the manager fails. func NewDMClient(_ js.Value, args []js.Value) any { privateIdentity := utils.CopyBytesToGo(args[1]) em := newDMReceiverBuilder(args[2]) cm, err := bindings.NewDMClient(args[0].Int(), privateIdentity, em) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -126,7 +135,7 @@ func NewDMClient(_ js.Value, args []js.Value) any { // Returns: // - Resolves to a Javascript representation of the [DMClient] object. // - Rejected with an error if loading indexedDbWorker or the manager fails. -// - Throws a TypeError if the cipher ID does not correspond to a cipher. +// - Throws an error if the cipher ID does not correspond to a cipher. func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any { cmixID := args[0].Int() wasmJsPath := args[1].String() @@ -136,7 +145,7 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any { cipher, err := bindings.GetDMDbCipherTrackerFromID(cipherID) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) } return newDMClientWithIndexedDb( @@ -194,19 +203,19 @@ func newDMClientWithIndexedDb(cmixID int, wasmJsPath string, pi, err := codename.UnmarshalPrivateIdentity(privateIdentity) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } dmPath := base64.RawStdEncoding.EncodeToString(pi.PubKey[:]) model, err := indexDB.NewWASMEventModel( dmPath, wasmJsPath, cipher, messageReceivedCB) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } cm, err := bindings.NewDMClientWithGoEventModel( cmixID, privateIdentity, model) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(newDMClientJS(cm)) } @@ -257,7 +266,7 @@ func (dmc *DMClient) GetIdentity(js.Value, []js.Value) any { func (dmc *DMClient) ExportPrivateIdentity(_ js.Value, args []js.Value) any { i, err := dmc.api.ExportPrivateIdentity(args[0].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -273,7 +282,7 @@ func (dmc *DMClient) ExportPrivateIdentity(_ js.Value, args []js.Value) any { func (dmc *DMClient) GetNickname(_ js.Value, _ []js.Value) any { nickname, err := dmc.api.GetNickname() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -357,7 +366,7 @@ func (dmc *DMClient) SendText(_ js.Value, args []js.Value) any { sendReport, err := dmc.api.SendText(partnerPubKeyBytes, partnerToken, message, leaseTimeMS, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -415,7 +424,7 @@ func (dmc *DMClient) SendReply(_ js.Value, args []js.Value) any { sendReport, err := dmc.api.SendReply(partnerPubKeyBytes, partnerToken, replyMessage, replyToBytes, leaseTimeMS, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -463,7 +472,43 @@ func (dmc *DMClient) SendReaction(_ js.Value, args []js.Value) any { sendReport, err := dmc.api.SendReaction(partnerPubKeyBytes, partnerToken, reaction, reactToBytes, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// SendSilent is used to send to a channel a message with no notifications. +// Its primary purpose is to communicate new nicknames without calling [Send]. +// +// It takes no payload intentionally as the message should be very lightweight. +// +// Parameters: +// - args[0] - The bytes of the public key of the partner's ED25519 +// signing key (Uint8Array). +// - args[1] - The token used to derive the reception ID for the partner +// (int). +// - args[2] - JSON of [xxdk.CMIXParams]. If left empty +// [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (dmc *DMClient) SendSilent(_ js.Value, args []js.Value) any { + var ( + partnerPubKeyBytes = utils.CopyBytesToGo(args[0]) + partnerToken = int32(args[1].Int()) + cmixParamsJSON = utils.CopyBytesToGo(args[2]) + ) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := dmc.api.SendSilent( + partnerPubKeyBytes, partnerToken, cmixParamsJSON) + if err != nil { + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -511,7 +556,7 @@ func (dmc *DMClient) SendInvite(_ js.Value, args []js.Value) any { partnerPubKeyBytes, partnerToken, marshalledInviteToId, msg, host, maxUses, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -562,7 +607,7 @@ func (dmc *DMClient) Send(_ js.Value, args []js.Value) any { sendReport, err := dmc.api.Send(partnerPubKeyBytes, partnerToken, messageType, plaintext, leaseTimeMS, cmixParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -581,6 +626,30 @@ func (dmc *DMClient) GetDatabaseName(js.Value, []js.Value) any { "_speakeasy_dm" } +// BlockSender blocks the provided sender public key from sending DMs +// +// Parameters: +// - args[0] - [ed25519.PublicKey] (Uint8Array) +// +// Returns nothing +func (dmc *DMClient) BlockSender(_ js.Value, args []js.Value) any { + senderKey := utils.CopyBytesToGo(args[0]) + dmc.api.BlockSender(senderKey) + return nil +} + +// UnblockSender unblocks the provided sender public key to allow sending DMs +// +// Parameters: +// - args[0] - [ed25519.PublicKey] (Uint8Array) +// +// Returns nothing +func (dmc *DMClient) UnblockSender(_ js.Value, args []js.Value) any { + senderKey := utils.CopyBytesToGo(args[0]) + dmc.api.UnblockSender(senderKey) + return nil +} + //////////////////////////////////////////////////////////////////////////////// // DM Share URL // //////////////////////////////////////////////////////////////////////////////// @@ -625,7 +694,7 @@ func (dmc *DMClient) GetShareURL(_ js.Value, args []js.Value) any { host := args[0].String() urlReport, err := dmc.api.GetShareURL(host) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -645,7 +714,7 @@ func DecodeDMShareURL(_ js.Value, args []js.Value) any { url := args[0].String() report, err := bindings.DecodeDMShareURL(url) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -676,7 +745,7 @@ func (cmrCB *dmReceptionCallback) Callback( receivedChannelMessageReport []byte, err error) int { uuid := cmrCB.callback( utils.CopyBytesToJS(receivedChannelMessageReport), - utils.JsTrace(err)) + exception.NewTrace(err)) return uuid.Int() } @@ -1008,7 +1077,7 @@ func newDMDbCipherJS(api *bindings.DMDbCipher) map[string]any { // // Returns: // - JavaScript representation of the [DMDbCipher] object. -// - Throws a TypeError if creating the cipher fails. +// - Throws an error if creating the cipher fails. func NewDMsDatabaseCipher(_ js.Value, args []js.Value) any { cmixId := args[0].Int() password := utils.CopyBytesToGo(args[1]) @@ -1017,7 +1086,7 @@ func NewDMsDatabaseCipher(_ js.Value, args []js.Value) any { cipher, err := bindings.NewDMsDatabaseCipher( cmixId, password, plaintTextBlockSize) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1043,11 +1112,11 @@ func (c *DMDbCipher) GetID(js.Value, []js.Value) any { // // Returns: // - The ciphertext of the plaintext passed in (Uint8Array). -// - Throws a TypeError if it fails to encrypt the plaintext. +// - Throws an error if it fails to encrypt the plaintext. func (c *DMDbCipher) Encrypt(_ js.Value, args []js.Value) any { ciphertext, err := c.api.Encrypt(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1064,11 +1133,11 @@ func (c *DMDbCipher) Encrypt(_ js.Value, args []js.Value) any { // // Returns: // - The plaintext of the ciphertext passed in (Uint8Array). -// - Throws a TypeError if it fails to encrypt the plaintext. +// - Throws an error if it fails to encrypt the plaintext. func (c *DMDbCipher) Decrypt(_ js.Value, args []js.Value) any { plaintext, err := c.api.Decrypt(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1079,11 +1148,11 @@ func (c *DMDbCipher) Decrypt(_ js.Value, args []js.Value) any { // // Returns: // - JSON of the cipher (Uint8Array). -// - Throws a TypeError if marshalling fails. +// - Throws an error if marshalling fails. func (c *DMDbCipher) MarshalJSON(js.Value, []js.Value) any { data, err := c.api.MarshalJSON() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -1101,11 +1170,11 @@ func (c *DMDbCipher) MarshalJSON(js.Value, []js.Value) any { // // Returns: // - JSON of the cipher (Uint8Array). -// - Throws a TypeError if marshalling fails. +// - Throws an error if marshalling fails. func (c *DMDbCipher) UnmarshalJSON(_ js.Value, args []js.Value) any { err := c.api.UnmarshalJSON(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return nil diff --git a/wasm/dummy.go b/wasm/dummy.go index 82b9128ce52cb3521056da6a77358bb44a4a5e5c..9efbc687eee46005192148f75f3164b81421a531 100644 --- a/wasm/dummy.go +++ b/wasm/dummy.go @@ -11,7 +11,7 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" "syscall/js" ) @@ -53,12 +53,12 @@ func newDummyTrafficJS(newDT *bindings.DummyTraffic) map[string]any { // // Returns: // - Javascript representation of the DummyTraffic object. -// - Throws a TypeError if creating the manager fails. +// - Throws an error if creating the manager fails. func NewDummyTrafficManager(_ js.Value, args []js.Value) any { dt, err := bindings.NewDummyTrafficManager( args[0].Int(), args[1].Int(), args[2].Int(), args[3].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -75,12 +75,12 @@ func NewDummyTrafficManager(_ js.Value, args []js.Value) any { // thread will then be prevented from beginning another round of sending. // // Returns: -// - Throws a TypeError if it fails to send a pause signal to the sending +// - Throws an error if it fails to send a pause signal to the sending // thread. func (dt *DummyTraffic) Pause(js.Value, []js.Value) any { err := dt.api.Pause() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -98,12 +98,12 @@ func (dt *DummyTraffic) Pause(js.Value, []js.Value) any { // sending interval after a call to Start. // // Returns: -// - Throws a TypeError if it fails to send a start signal to the sending +// - Throws an error if it fails to send a start signal to the sending // thread. func (dt *DummyTraffic) Start(js.Value, []js.Value) any { err := dt.api.Start() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/e2e.go b/wasm/e2e.go index 8d8f4ce15fa0c47f71c3bbbeccdf9484a4ccde39..dcb4853d23745f0d85a879284a6b2c918a1d5f39 100644 --- a/wasm/e2e.go +++ b/wasm/e2e.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -90,7 +91,7 @@ func (e *E2e) GetID(js.Value, []js.Value) any { // // Returns: // - Javascript representation of the [E2e] object. -// - Throws a TypeError if logging in fails. +// - Throws an error if logging in fails. func Login(_ js.Value, args []js.Value) any { callbacks := newAuthCallbacks(args[1]) identity := utils.CopyBytesToGo(args[2]) @@ -99,7 +100,7 @@ func Login(_ js.Value, args []js.Value) any { newE2E, err := bindings.Login( args[0].Int(), callbacks, identity, e2eParamsJSON) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -120,7 +121,7 @@ func Login(_ js.Value, args []js.Value) any { // // Returns: // - Javascript representation of the [E2e] object. -// - Throws a TypeError if logging in fails. +// - Throws an error if logging in fails. func LoginEphemeral(_ js.Value, args []js.Value) any { callbacks := newAuthCallbacks(args[1]) identity := utils.CopyBytesToGo(args[2]) @@ -129,7 +130,7 @@ func LoginEphemeral(_ js.Value, args []js.Value) any { newE2E, err := bindings.LoginEphemeral( args[0].Int(), callbacks, identity, e2eParamsJSON) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -167,11 +168,11 @@ func (e *E2e) GetUdCertFromNdf(js.Value, []js.Value) any { // // Returns // - Marshalled bytes of [contact.Contact] (Uint8Array). -// - Throws a TypeError if the contact file cannot be loaded. +// - Throws an error if the contact file cannot be loaded. func (e *E2e) GetUdContactFromNdf(js.Value, []js.Value) any { b, err := e.api.GetUdContactFromNdf() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/e2eAuth.go b/wasm/e2eAuth.go index eb69ef75cd17d26b22715d0eba08bf6b5f0fc02a..351ed123db2b69c315ceb61bcd5885146149e3dd 100644 --- a/wasm/e2eAuth.go +++ b/wasm/e2eAuth.go @@ -10,7 +10,8 @@ package wasm import ( - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -46,7 +47,7 @@ func (e *E2e) Request(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { rid, err := e.api.Request(partnerContact, factsListJson) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(rid) } @@ -83,7 +84,7 @@ func (e *E2e) Confirm(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { rid, err := e.api.Confirm(partnerContact) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(rid) } @@ -118,7 +119,7 @@ func (e *E2e) Reset(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { rid, err := e.api.Reset(partnerContact) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(rid) } @@ -147,7 +148,7 @@ func (e *E2e) ReplayConfirm(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { rid, err := e.api.ReplayConfirm(partnerContact) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(rid) } @@ -174,7 +175,7 @@ func (e *E2e) DeleteRequest(_ js.Value, args []js.Value) any { partnerContact := utils.CopyBytesToGo(args[0]) err := e.api.DeleteRequest(partnerContact) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -188,7 +189,7 @@ func (e *E2e) DeleteRequest(_ js.Value, args []js.Value) any { func (e *E2e) DeleteAllRequests(js.Value, []js.Value) any { err := e.api.DeleteAllRequests() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -202,7 +203,7 @@ func (e *E2e) DeleteAllRequests(js.Value, []js.Value) any { func (e *E2e) DeleteSentRequests(js.Value, []js.Value) any { err := e.api.DeleteSentRequests() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -216,7 +217,7 @@ func (e *E2e) DeleteSentRequests(js.Value, []js.Value) any { func (e *E2e) DeleteReceiveRequests(js.Value, []js.Value) any { err := e.api.DeleteReceiveRequests() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -235,7 +236,7 @@ func (e *E2e) GetReceivedRequest(_ js.Value, args []js.Value) any { partnerContact := utils.CopyBytesToGo(args[0]) c, err := e.api.GetReceivedRequest(partnerContact) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -260,7 +261,7 @@ func (e *E2e) VerifyOwnership(_ js.Value, args []js.Value) any { isValid, err := e.api.VerifyOwnership( receivedContact, verifiedContact, args[2].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -282,7 +283,7 @@ func (e *E2e) AddPartnerCallback(_ js.Value, args []js.Value) any { callbacks := newAuthCallbacks(args[1]) err := e.api.AddPartnerCallback(partnerID, callbacks) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -301,7 +302,7 @@ func (e *E2e) DeletePartnerCallback(_ js.Value, args []js.Value) any { partnerID := utils.CopyBytesToGo(args[0]) err := e.api.DeletePartnerCallback(partnerID) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/e2eHandler.go b/wasm/e2eHandler.go index bff44e6aa48567e5c6afae5dca439b6fa93a6fa1..e483fc7e46cf9e0624881aefaf80a9c5ffcae2af 100644 --- a/wasm/e2eHandler.go +++ b/wasm/e2eHandler.go @@ -10,8 +10,10 @@ package wasm import ( - "gitlab.com/elixxir/xxdk-wasm/utils" "syscall/js" + + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" ) // GetReceptionID returns the marshalled default IDs. @@ -32,7 +34,7 @@ func (e *E2e) GetReceptionID(js.Value, []js.Value) any { func (e *E2e) DeleteContact(_ js.Value, args []js.Value) any { err := e.api.DeleteContact(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return nil @@ -47,7 +49,7 @@ func (e *E2e) DeleteContact(_ js.Value, args []js.Value) any { func (e *E2e) GetAllPartnerIDs(js.Value, []js.Value) any { partnerIDs, err := e.api.GetAllPartnerIDs() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return utils.CopyBytesToJS(partnerIDs) @@ -100,7 +102,7 @@ func (e *E2e) FirstPartitionSize(js.Value, []js.Value) any { func (e *E2e) GetHistoricalDHPrivkey(js.Value, []js.Value) any { privKey, err := e.api.GetHistoricalDHPrivkey() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return utils.CopyBytesToJS(privKey) @@ -115,7 +117,7 @@ func (e *E2e) GetHistoricalDHPrivkey(js.Value, []js.Value) any { func (e *E2e) GetHistoricalDHPubkey(js.Value, []js.Value) any { pubKey, err := e.api.GetHistoricalDHPubkey() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return utils.CopyBytesToJS(pubKey) @@ -133,7 +135,7 @@ func (e *E2e) GetHistoricalDHPubkey(js.Value, []js.Value) any { func (e *E2e) HasAuthenticatedChannel(_ js.Value, args []js.Value) any { exists, err := e.api.HasAuthenticatedChannel(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } return exists @@ -149,7 +151,7 @@ func (e *E2e) HasAuthenticatedChannel(_ js.Value, args []js.Value) any { func (e *E2e) RemoveService(_ js.Value, args []js.Value) any { err := e.api.RemoveService(args[0].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -178,7 +180,7 @@ func (e *E2e) SendE2E(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { sendReport, err := e.api.SendE2E(mt, recipientId, payload, e2eParams) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -199,13 +201,18 @@ type processor struct { // // Parameters: // - message - Returns the message contents (Uint8Array). +// - tags - The tags on the message (Uint8Array). +// - metadata - Other arbitrary metadata (Uint8Array). // - receptionId - Returns the marshalled bytes of the sender's [id.ID] // (Uint8Array). // - ephemeralId - Returns the ephemeral ID of the sender (int). // - roundId - Returns the ID of the round sent on (int). -func (p *processor) Process( - message, receptionId []byte, ephemeralId, roundId int64) { - p.process(utils.CopyBytesToJS(message), utils.CopyBytesToJS(receptionId), +func (p *processor) Process(message, tags, metadata, receptionId []byte, + ephemeralId, roundId int64) { + p.process(utils.CopyBytesToJS(message), + utils.CopyBytesToJS(tags), + utils.CopyBytesToJS(metadata), + utils.CopyBytesToJS(receptionId), ephemeralId, roundId) } @@ -232,11 +239,13 @@ func (p *processor) String() string { // - Throws TypeError if registering the service fails. func (e *E2e) AddService(_ js.Value, args []js.Value) any { p := &processor{ - utils.WrapCB(args[1], "Process"), utils.WrapCB(args[1], "String")} + utils.WrapCB(args[1], "Process"), + utils.WrapCB(args[1], "String"), + } err := e.api.AddService(args[0].String(), p) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -261,7 +270,7 @@ func (e *E2e) RegisterListener(_ js.Value, args []js.Value) any { err := e.api.RegisterListener(recipientId, args[1].Int(), l) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/emoji.go b/wasm/emoji.go index fbde2b5717a4c745fc7f7339d96f157703138cad..7be8ce8e45461c4e2ffb1a0f1f215cff313421a4 100644 --- a/wasm/emoji.go +++ b/wasm/emoji.go @@ -13,7 +13,8 @@ import ( "syscall/js" "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" ) // SupportedEmojis returns a list of emojis that are supported by the backend. @@ -21,7 +22,7 @@ import ( // // Returns: // - JSON of an array of emoji.Emoji (Uint8Array). -// - Throws a TypeError if marshalling the JSON fails. +// - Throws an error if marshalling the JSON fails. // // Example JSON: // @@ -56,7 +57,7 @@ import ( func SupportedEmojis(js.Value, []js.Value) any { data, err := bindings.SupportedEmojis() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -68,7 +69,7 @@ func SupportedEmojis(js.Value, []js.Value) any { // // Returns: // - JSON of a map of emoji.Emoji (Uint8Array). -// - Throws a TypeError if marshalling the JSON fails. +// - Throws an error if marshalling the JSON fails. // // Example JSON: // @@ -101,7 +102,7 @@ func SupportedEmojis(js.Value, []js.Value) any { func SupportedEmojisMap(js.Value, []js.Value) any { data, err := bindings.SupportedEmojisMap() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -121,7 +122,7 @@ func SupportedEmojisMap(js.Value, []js.Value) any { func ValidateReaction(_ js.Value, args []js.Value) any { err := bindings.ValidateReaction(args[0].String()) if err != nil { - return utils.JsError(err) + return exception.NewError(err) } return nil diff --git a/wasm/errors.go b/wasm/errors.go index 9299c60e0d9c02e81a1ea54755c26d7be62b87bf..2b2bab8c2ce6ebc762bad96c8e317de313207e6f 100644 --- a/wasm/errors.go +++ b/wasm/errors.go @@ -11,7 +11,7 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" "syscall/js" ) @@ -49,11 +49,11 @@ func CreateUserFriendlyErrorMessage(_ js.Value, args []js.Value) any { // } // // Returns: -// - Throws a TypeError if the JSON cannot be unmarshalled. +// - Throws an error if the JSON cannot be unmarshalled. func UpdateCommonErrors(_ js.Value, args []js.Value) any { err := bindings.UpdateCommonErrors(args[0].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/fileTransfer.go b/wasm/fileTransfer.go index 0083a98a78b2544f90b96623b88157b9dd70acb2..033c410c1231685db46a9c88ea999d1c2be727f3 100644 --- a/wasm/fileTransfer.go +++ b/wasm/fileTransfer.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -84,7 +85,7 @@ type fileTransferSentProgressCallback struct { func (spc *fileTransferSentProgressCallback) Callback( payload []byte, t *bindings.FilePartTracker, err error) { spc.callback(utils.CopyBytesToJS(payload), newFilePartTrackerJS(t), - utils.JsTrace(err)) + exception.NewTrace(err)) } // fileTransferReceiveProgressCallback wraps Javascript callbacks to adhere to @@ -105,7 +106,7 @@ type fileTransferReceiveProgressCallback struct { func (rpc *fileTransferReceiveProgressCallback) Callback( payload []byte, t *bindings.FilePartTracker, err error) { rpc.callback(utils.CopyBytesToJS(payload), newFilePartTrackerJS(t), - utils.JsTrace(err)) + exception.NewTrace(err)) } //////////////////////////////////////////////////////////////////////////////// @@ -124,7 +125,7 @@ func (rpc *fileTransferReceiveProgressCallback) Callback( // // Returns: // - Javascript representation of the [FileTransfer] object. -// - Throws a TypeError initialising the file transfer manager fails. +// - Throws an error initialising the file transfer manager fails. func InitFileTransfer(_ js.Value, args []js.Value) any { rfc := &receiveFileCallback{utils.WrapCB(args[1], "Callback")} e2eFileTransferParamsJson := utils.CopyBytesToGo(args[2]) @@ -133,7 +134,7 @@ func InitFileTransfer(_ js.Value, args []js.Value) any { api, err := bindings.InitFileTransfer( args[0].Int(), rfc, e2eFileTransferParamsJson, fileTransferParamsJson) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -163,7 +164,7 @@ func (f *FileTransfer) Send(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { ftID, err := f.api.Send(payload, recipientID, retry, spc, args[4].Int()) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(ftID)) } @@ -185,12 +186,12 @@ func (f *FileTransfer) Send(_ js.Value, args []js.Value) any { // // Returns: // - File contents (Uint8Array). -// - Throws a TypeError the file transfer is incomplete or Receive has already +// - Throws an error the file transfer is incomplete or Receive has already // been called. func (f *FileTransfer) Receive(_ js.Value, args []js.Value) any { file, err := f.api.Receive(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -208,11 +209,11 @@ func (f *FileTransfer) Receive(_ js.Value, args []js.Value) any { // - args[0] - File transfer [fileTransfer.TransferID] (Uint8Array). // // Returns: -// - Throws a TypeError if the file transfer is incomplete. +// - Throws an error if the file transfer is incomplete. func (f *FileTransfer) CloseSend(_ js.Value, args []js.Value) any { err := f.api.CloseSend(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -237,7 +238,7 @@ func (f *FileTransfer) CloseSend(_ js.Value, args []js.Value) any { // triggering (int). // // Returns: -// - Throws a TypeError if registering the callback fails. +// - Throws an error if registering the callback fails. func (f *FileTransfer) RegisterSentProgressCallback( _ js.Value, args []js.Value) any { tidBytes := utils.CopyBytesToGo(args[0]) @@ -245,7 +246,7 @@ func (f *FileTransfer) RegisterSentProgressCallback( err := f.api.RegisterSentProgressCallback(tidBytes, spc, args[2].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -265,7 +266,7 @@ func (f *FileTransfer) RegisterSentProgressCallback( // triggering (int). // // Returns: -// - Throws a TypeError if registering the callback fails. +// - Throws an error if registering the callback fails. func (f *FileTransfer) RegisterReceivedProgressCallback( _ js.Value, args []js.Value) any { tidBytes := utils.CopyBytesToGo(args[0]) @@ -274,7 +275,7 @@ func (f *FileTransfer) RegisterReceivedProgressCallback( err := f.api.RegisterReceivedProgressCallback( tidBytes, rpc, args[2].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/follow.go b/wasm/follow.go index 7f573609e037aa3a7c78f286b3cb3e51de016bc5..a0fdeb73b4db5926ddba8f604bf7583fe07c7ac8 100644 --- a/wasm/follow.go +++ b/wasm/follow.go @@ -10,9 +10,11 @@ package wasm import ( - "gitlab.com/elixxir/xxdk-wasm/storage" - "gitlab.com/elixxir/xxdk-wasm/utils" "syscall/js" + + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" + "gitlab.com/elixxir/xxdk-wasm/storage" ) // StartNetworkFollower kicks off the tracking of the network. It starts long- @@ -53,11 +55,11 @@ import ( // - args[0] - Timeout when stopping threads in milliseconds (int). // // Returns: -// - Throws a TypeError if starting the network follower fails. +// - Throws an error if starting the network follower fails. func (c *Cmix) StartNetworkFollower(_ js.Value, args []js.Value) any { err := c.api.StartNetworkFollower(args[0].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -71,12 +73,12 @@ func (c *Cmix) StartNetworkFollower(_ js.Value, args []js.Value) any { // most likely be in an unrecoverable state and need to be trashed. // // Returns: -// - Throws a TypeError if the follower is in the wrong state to stop or if it +// - Throws an error if the follower is in the wrong state to stop or if it // fails to stop. func (c *Cmix) StopNetworkFollower(js.Value, []js.Value) any { err := c.api.StopNetworkFollower() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -87,6 +89,21 @@ func (c *Cmix) StopNetworkFollower(js.Value, []js.Value) any { // SetTrackNetworkPeriod allows changing the frequency that follower threads // are started. // +// Note that the frequency of the follower threads affect the power usage +// of the device following the network. +// - Low period -> Higher frequency of polling -> Higher battery usage +// - High period -> Lower frequency of polling -> Lower battery usage +// +// This may be used to enable a low power (or battery optimization) mode +// for the end user. +// +// Suggested values are provided, however there are no guarantees that these +// values will perfectly fit what the end user's device would require to match +// the user's expectations: +// - Low Power Usage: 5000 milliseconds +// - High Power Usage: 1000 milliseconds (default, see +// [cmix.DefaultFollowPeriod] +// // Parameters: // - args[0] - The duration of the period, in milliseconds (int). func (c *Cmix) SetTrackNetworkPeriod(_ js.Value, args []js.Value) any { @@ -150,7 +167,7 @@ func (c *Cmix) NetworkFollowerStatus(js.Value, []js.Value) any { func (c *Cmix) GetNodeRegistrationStatus(js.Value, []js.Value) any { b, err := c.api.GetNodeRegistrationStatus() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -171,7 +188,7 @@ func (c *Cmix) GetNodeRegistrationStatus(js.Value, []js.Value) any { func (c *Cmix) IsReady(_ js.Value, args []js.Value) any { isReadyInfo, err := c.api.IsReady(args[0].Float()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -190,7 +207,7 @@ func (c *Cmix) IsReady(_ js.Value, args []js.Value) any { func (c *Cmix) PauseNodeRegistrations(_ js.Value, args []js.Value) any { err := c.api.PauseNodeRegistrations(args[0].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -210,7 +227,7 @@ func (c *Cmix) PauseNodeRegistrations(_ js.Value, args []js.Value) any { func (c *Cmix) ChangeNumberOfNodeRegistrations(_ js.Value, args []js.Value) any { err := c.api.ChangeNumberOfNodeRegistrations(args[0].Int(), args[1].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -256,7 +273,7 @@ func (c *Cmix) IsHealthy(js.Value, []js.Value) any { func (c *Cmix) GetRunningProcesses(js.Value, []js.Value) any { list, err := c.api.GetRunningProcesses() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -362,7 +379,52 @@ type trackServicesCallback struct { // }, // ] func (tsc *trackServicesCallback) Callback(marshalData []byte, err error) { - tsc.callback(utils.CopyBytesToJS(marshalData), utils.JsTrace(err)) + tsc.callback(utils.CopyBytesToJS(marshalData), exception.NewTrace(err)) +} + +// trackCompressedServicesCallback adheres to the +// [bindings.TrackCompressedServicesCallback] interface. +type trackCompressedServicesCallback struct { + callback func(args ...any) js.Value +} + +// Callback is the callback for [Cmix.TrackServices] that passes a +// JSON-marshalled list of compressed backend services. If an error occurs while +// retrieving or marshalling the service list, then err will be non-null. +// +// Parameters: +// - marshalData - JSON of [message.CompressedServiceList] (Uint8Array), +// which is a map of [id.ID] to an array of [message.CompressedService]. +// - err - Error that occurs during retrieval or marshalling. Null otherwise +// (Error). +// +// Example JSON: +// +// { +// "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD": [ +// { +// "Identifier": null, +// "Tags": ["test"], +// "Metadata": null +// } +// ], +// "AAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD": [ +// { +// "Identifier": null, +// "Tags": ["test"], +// "Metadata": null +// } +// ], +// "AAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD": [ +// { +// "Identifier": null, +// "Tags": ["test"], +// "Metadata": null +// } +// ] +// } +func (tsc *trackCompressedServicesCallback) Callback(marshalData []byte, err error) { + tsc.callback(utils.CopyBytesToJS(marshalData), exception.NewTrace(err)) } // TrackServicesWithIdentity will return via a callback the list of services the @@ -374,14 +436,18 @@ func (tsc *trackServicesCallback) Callback(marshalData []byte, err error) { // - args[0] - ID of [E2e] object in tracker (int). // - args[1] - Javascript object that has functions that implement the // [bindings.ClientError] interface. +// - args[2] - Javascript object that has functions that implement the +// [bindings.TrackCompressedServicesCallback], which will be passed the JSON +// of [message.CompressedServiceList]. // // Returns: // - Throws TypeError if the [E2e] ID is invalid. func (c *Cmix) TrackServicesWithIdentity(_ js.Value, args []js.Value) any { err := c.api.TrackServicesWithIdentity(args[0].Int(), - &trackServicesCallback{utils.WrapCB(args[0], "Callback")}) + &trackServicesCallback{utils.WrapCB(args[0], "Callback")}, + &trackCompressedServicesCallback{utils.WrapCB(args[0], "Callback")}) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/group.go b/wasm/group.go index f3e7710a9144ba86fd33322b05c44092dd5c89bc..e824d4ee85ce7610f99e6e63dba6ab56e0e58f2a 100644 --- a/wasm/group.go +++ b/wasm/group.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -54,7 +55,7 @@ func newGroupChatJS(api *bindings.GroupChat) map[string]any { // // Returns: // - Javascript representation of the [GroupChat] object. -// - Throws a TypeError if creating the [GroupChat] fails. +// - Throws an error if creating the [GroupChat] fails. func NewGroupChat(_ js.Value, args []js.Value) any { requestFunc := &groupRequest{utils.WrapCB(args[1], "Callback")} p := &groupChatProcessor{ @@ -62,7 +63,7 @@ func NewGroupChat(_ js.Value, args []js.Value) any { api, err := bindings.NewGroupChat(args[0].Int(), requestFunc, p) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -93,7 +94,7 @@ func (g *GroupChat) MakeGroup(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { sendReport, err := g.api.MakeGroup(membershipBytes, message, name) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -117,7 +118,7 @@ func (g *GroupChat) ResendRequest(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { sendReport, err := g.api.ResendRequest(groupId) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -135,11 +136,11 @@ func (g *GroupChat) ResendRequest(_ js.Value, args []js.Value) any { // object returned over the bindings (Uint8Array). // // Returns: -// - Throws a TypeError if joining the group fails. +// - Throws an error if joining the group fails. func (g *GroupChat) JoinGroup(_ js.Value, args []js.Value) any { err := g.api.JoinGroup(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -153,11 +154,11 @@ func (g *GroupChat) JoinGroup(_ js.Value, args []js.Value) any { // can be found in the report returned by [GroupChat.MakeGroup]. // // Returns: -// - Throws a TypeError if leaving the group fails. +// - Throws an error if leaving the group fails. func (g *GroupChat) LeaveGroup(_ js.Value, args []js.Value) any { err := g.api.LeaveGroup(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -187,7 +188,7 @@ func (g *GroupChat) Send(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { sendReport, err := g.api.Send(groupId, message, tag) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -200,11 +201,11 @@ func (g *GroupChat) Send(_ js.Value, args []js.Value) any { // // Returns: // - JSON of array of [id.ID] representing all group ID's (Uint8Array). -// - Throws a TypeError if getting the groups fails. +// - Throws an error if getting the groups fails. func (g *GroupChat) GetGroups(js.Value, []js.Value) any { groups, err := g.api.GetGroups() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -220,11 +221,11 @@ func (g *GroupChat) GetGroups(js.Value, []js.Value) any { // // Returns: // - Javascript representation of the [GroupChat] object. -// - Throws a TypeError if getting the group fails. +// - Throws an error if getting the group fails. func (g *GroupChat) GetGroup(_ js.Value, args []js.Value) any { grp, err := g.api.GetGroup(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -314,11 +315,11 @@ func (g *Group) GetCreatedMS(js.Value, []js.Value) any { // // Returns: // - JSON of [group.Membership] (Uint8Array). -// - Throws a TypeError if marshalling fails. +// - Throws an error if marshalling fails. func (g *Group) GetMembership(js.Value, []js.Value) any { membership, err := g.api.GetMembership() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -341,11 +342,11 @@ func (g *Group) Serialize(js.Value, []js.Value) any { // // Returns: // - Javascript representation of the [GroupChat] object. -// - Throws a TypeError if getting the group fails. +// - Throws an error if getting the group fails. func DeserializeGroup(_ js.Value, args []js.Value) any { grp, err := bindings.DeserializeGroup(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -393,7 +394,7 @@ func (gcp *groupChatProcessor) Process(decryptedMessage, msg, receptionId []byte, ephemeralId, roundId int64, roundURL string, err error) { gcp.process(utils.CopyBytesToJS(decryptedMessage), utils.CopyBytesToJS(msg), utils.CopyBytesToJS(receptionId), ephemeralId, - roundId, roundURL, utils.JsTrace(err)) + roundId, roundURL, exception.NewTrace(err)) } // String returns a name identifying this processor. Used for debugging. diff --git a/wasm/identity.go b/wasm/identity.go index 4141b1f1b9dc0b6a2cf0f374b428118b19899363..0800fffa8d209ab1bfd2c004998ade87c45cf2b3 100644 --- a/wasm/identity.go +++ b/wasm/identity.go @@ -12,7 +12,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/client/v4/xxdk" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -31,14 +32,14 @@ import ( // - args[2] - ID of [Cmix] object in tracker (int). // // Returns: -// - Throws a TypeError if the identity cannot be stored in storage. +// - Throws an error if the identity cannot be stored in storage. func StoreReceptionIdentity(_ js.Value, args []js.Value) any { identity := utils.CopyBytesToGo(args[1]) err := bindings.StoreReceptionIdentity( args[0].String(), identity, args[2].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -54,11 +55,11 @@ func StoreReceptionIdentity(_ js.Value, args []js.Value) any { // // Returns: // - JSON of the stored [xxdk.ReceptionIdentity] object (Uint8Array). -// - Throws a TypeError if the identity cannot be retrieved from storage. +// - Throws an error if the identity cannot be retrieved from storage. func LoadReceptionIdentity(_ js.Value, args []js.Value) any { ri, err := bindings.LoadReceptionIdentity(args[0].String(), args[1].Int()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -75,7 +76,7 @@ func (c *Cmix) MakeReceptionIdentity(js.Value, []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { ri, err := c.api.MakeReceptionIdentity() if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(ri)) } @@ -94,7 +95,7 @@ func (c *Cmix) MakeLegacyReceptionIdentity(js.Value, []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { ri, err := c.api.MakeLegacyReceptionIdentity() if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(ri)) } @@ -126,13 +127,13 @@ func (c *Cmix) GetReceptionRegistrationValidationSignature( // // Returns: // - Marshalled bytes of [contact.Contact] (string). -// - Throws a TypeError if unmarshalling the identity fails. +// - Throws an error if unmarshalling the identity fails. func GetContactFromReceptionIdentity(_ js.Value, args []js.Value) any { // Note that this function does not appear in normal bindings identityJSON := utils.CopyBytesToGo(args[0]) identity, err := xxdk.UnmarshalReceptionIdentity(identityJSON) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -146,11 +147,11 @@ func GetContactFromReceptionIdentity(_ js.Value, args []js.Value) any { // // Returns: // - Marshalled bytes of [id.ID] (Uint8Array). -// - Throws a TypeError if loading the ID from the contact file fails. +// - Throws an error if loading the ID from the contact file fails. func GetIDFromContact(_ js.Value, args []js.Value) any { cID, err := bindings.GetIDFromContact(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -165,11 +166,11 @@ func GetIDFromContact(_ js.Value, args []js.Value) any { // // Returns: // - Bytes of the [cyclic.Int] object (Uint8Array). -// - Throws a TypeError if loading the public key from the contact file fails. +// - Throws an error if loading the public key from the contact file fails. func GetPubkeyFromContact(_ js.Value, args []js.Value) any { key, err := bindings.GetPubkeyFromContact([]byte(args[0].String())) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -189,13 +190,13 @@ func GetPubkeyFromContact(_ js.Value, args []js.Value) any { // // Returns: // - Marshalled bytes of the modified [contact.Contact] (string). -// - Throws a TypeError if loading or modifying the contact fails. +// - Throws an error if loading or modifying the contact fails. func SetFactsOnContact(_ js.Value, args []js.Value) any { marshaledContact := utils.CopyBytesToGo(args[0]) factListJSON := utils.CopyBytesToGo(args[1]) c, err := bindings.SetFactsOnContact(marshaledContact, factListJSON) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -209,11 +210,11 @@ func SetFactsOnContact(_ js.Value, args []js.Value) any { // // Returns: // - JSON of [fact.FactList] (Uint8Array). -// - Throws a TypeError if loading the contact fails. +// - Throws an error if loading the contact fails. func GetFactsFromContact(_ js.Value, args []js.Value) any { fl, err := bindings.GetFactsFromContact(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } diff --git a/wasm/logging.go b/wasm/logging.go index 8199edc02a829a8e8b81b59dbfc73cddb4d46857..1ae9bb7127447f1a786833788eb34250ed783979 100644 --- a/wasm/logging.go +++ b/wasm/logging.go @@ -10,35 +10,10 @@ package wasm import ( - "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/logging" "syscall/js" -) -// LogLevel sets level of logging. All logs at the set level and below will be -// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL -// messages will be printed). -// -// Log level options: -// -// TRACE - 0 -// DEBUG - 1 -// INFO - 2 -// WARN - 3 -// ERROR - 4 -// CRITICAL - 5 -// FATAL - 6 -// -// The default log level without updates is INFO. -// -// Parameters: -// - args[0] - Log level (int). -// -// Returns: -// - Throws TypeError if the log level is invalid. -func LogLevel(this js.Value, args []js.Value) any { - return logging.LogLevelJS(this, args) -} + "gitlab.com/elixxir/client/v4/bindings" +) // logWriter wraps Javascript callbacks to adhere to the [bindings.LogWriter] // interface. diff --git a/wasm/ndf.go b/wasm/ndf.go index 69441fea2c03e5f369a0e9a76a5936b7e43bf67f..292fe3ecfb42d57a2a7964c03265bdfd7c39d14c 100644 --- a/wasm/ndf.go +++ b/wasm/ndf.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -34,7 +35,7 @@ func DownloadAndVerifySignedNdfWithUrl(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { ndf, err := bindings.DownloadAndVerifySignedNdfWithUrl(url, cert) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(ndf)) } diff --git a/wasm/notifications.go b/wasm/notifications.go new file mode 100644 index 0000000000000000000000000000000000000000..26154318b16c4b4e9f54270ea97b40ec7faaa32d --- /dev/null +++ b/wasm/notifications.go @@ -0,0 +1,130 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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/wasm-utils/exception" +) + +type Notifications struct { + api *bindings.Notifications +} + +// newNotificationsJS wrapts the bindings Noticiation object and implements +// wrappers in JS for all it's functionality. +func newNotificationsJS(api *bindings.Notifications) map[string]any { + n := Notifications{api} + notificationsImplJS := map[string]any{ + "AddToken": js.FuncOf(n.AddToken), + "RemoveToken": js.FuncOf(n.RemoveToken), + "SetMaxState": js.FuncOf(n.SetMaxState), + "GetMaxState": js.FuncOf(n.GetMaxState), + "GetID": js.FuncOf(n.GetID), + } + return notificationsImplJS +} + +// LoadNotifications returns a JS wrapped implementation of +// [bindings.Notifications]. +// +// Parameters: +// - args[0] - the cMixID integer +// +// Returns a notifications object or throws an error +func LoadNotifications(_ js.Value, args []js.Value) any { + cMixID := args[0].Int() + api, err := bindings.LoadNotifications(cMixID) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + return newNotificationsJS(api) +} + +// LoadNotificationsDummy returns a JS wrapped implementation of +// [bindings.Notifications] with a dummy notifications implementation. +// +// Parameters: +// - args[0] - the cMixID integer +// +// Returns a notifications object or throws an error +func LoadNotificationsDummy(_ js.Value, args []js.Value) any { + cMixID := args[0].Int() + api, err := bindings.LoadNotificationsDummy(cMixID) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + return newNotificationsJS(api) +} + +// GetID returns the bindings ID for the [bindings.Notifications] object +func (n *Notifications) GetID(js.Value, []js.Value) any { + return n.api.GetID() +} + +// AddToken implements [bindings.Notifications.AddToken]. +// +// Parameters: +// - args[0] - newToken string +// - args[1] - app string +// +// Returns nothing or an error (throwable) +func (n *Notifications) AddToken(_ js.Value, args []js.Value) any { + newToken := args[0].String() + app := args[1].String() + + err := n.api.AddToken(newToken, app) + if err != nil { + exception.ThrowTrace(err) + } + + return nil +} + +// RemoveToken implements [bindings.Notifications.RemoveToken]. +// +// Returns nothing or throws an error. +func (n *Notifications) RemoveToken(_ js.Value, args []js.Value) any { + err := n.api.RemoveToken() + if err != nil { + exception.ThrowTrace(err) + } + return nil +} + +// SetMaxState implements [bindings.Notifications.SetMaxState] +// +// Parameters: +// - args[0] - maxState integer +// +// Returns nothing or throws an error +func (n *Notifications) SetMaxState(_ js.Value, args []js.Value) any { + maxState := int64(args[0].Int()) + + err := n.api.SetMaxState(maxState) + if err != nil { + exception.ThrowTrace(err) + } + + return nil +} + +// GetMaxState implements [bindings.Notifications.GetMaxState] +// +// Returns the current maxState integer +func (n *Notifications) GetMaxState(_ js.Value, args []js.Value) any { + return int64(n.api.GetMaxState()) +} diff --git a/wasm/params.go b/wasm/params.go index 01f307c6c029f1643e674fe7b77db44d4830b2fe..5b8dac2f78656de84be3050705cc4c72415a5ee7 100644 --- a/wasm/params.go +++ b/wasm/params.go @@ -11,7 +11,7 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) diff --git a/wasm/restlike.go b/wasm/restlike.go index 0cf802a32bb0c638859c77d1a32fc609cd2994b8..d56ff586279f292042d2faa7df39d38714273272 100644 --- a/wasm/restlike.go +++ b/wasm/restlike.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -39,7 +40,7 @@ func RestlikeRequest(_ js.Value, args []js.Value) any { msg, err := bindings.RestlikeRequest( cmixId, connectionID, request, e2eParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(msg)) } @@ -72,7 +73,7 @@ func RestlikeRequestAuth(_ js.Value, args []js.Value) any { msg, err := bindings.RestlikeRequestAuth( cmixId, authConnectionID, request, e2eParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(msg)) } diff --git a/wasm/restlikeSingle.go b/wasm/restlikeSingle.go index 391e40c01a2711aa630468ae59d06e15b5312bab..41118d7f6d6ae32960712ea2f0436a927d28255d 100644 --- a/wasm/restlikeSingle.go +++ b/wasm/restlikeSingle.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -27,7 +28,7 @@ type restlikeCallback struct { // - payload - JSON of [restlike.Message] (Uint8Array). // - err - Returns an error on failure (Error). func (rlc *restlikeCallback) Callback(payload []byte, err error) { - rlc.callback(utils.CopyBytesToJS(payload), utils.JsTrace(err)) + rlc.callback(utils.CopyBytesToJS(payload), exception.NewTrace(err)) } // RequestRestLike sends a restlike request to a given contact. @@ -54,7 +55,7 @@ func RequestRestLike(_ js.Value, args []js.Value) any { msg, err := bindings.RequestRestLike( e2eID, recipient, request, paramsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(msg)) } @@ -79,7 +80,7 @@ func RequestRestLike(_ js.Value, args []js.Value) any { // [bindings.RestlikeCallback] interface. // // Returns: -// - Throws a TypeError if parsing the parameters or making the request fails. +// - Throws an error if parsing the parameters or making the request fails. func AsyncRequestRestLike(_ js.Value, args []js.Value) any { e2eID := args[0].Int() recipient := utils.CopyBytesToGo(args[1]) @@ -91,7 +92,7 @@ func AsyncRequestRestLike(_ js.Value, args []js.Value) any { err := bindings.AsyncRequestRestLike( e2eID, recipient, request, paramsJSON, cb) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) } }() diff --git a/wasm/secrets.go b/wasm/secrets.go index d310fcbcd4cd7e2593ec6d360274ed5cf9eea48c..4cba18e0495a251eb7a803bafd968c8f16757e50 100644 --- a/wasm/secrets.go +++ b/wasm/secrets.go @@ -11,7 +11,7 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) diff --git a/wasm/single.go b/wasm/single.go index 8198bb80fc58de0b21a045c47b39194efd149cfa..3528a0bea1d27896c28e35c94960a9cf10fa1034 100644 --- a/wasm/single.go +++ b/wasm/single.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -49,7 +50,7 @@ func TransmitSingleUse(_ js.Value, args []js.Value) any { sendReport, err := bindings.TransmitSingleUse( e2eID, recipient, tag, payload, paramsJSON, responseCB) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -71,12 +72,12 @@ func TransmitSingleUse(_ js.Value, args []js.Value) any { // Returns: // - Javascript representation of the [Stopper] object, an interface // containing a function used to stop the listener. -// - Throws a TypeError if listening fails. +// - Throws an error if listening fails. func Listen(_ js.Value, args []js.Value) any { cb := &singleUseCallback{utils.WrapCB(args[2], "Callback")} api, err := bindings.Listen(args[0].Int(), args[1].String(), cb) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -128,7 +129,7 @@ type singleUseCallback struct { // (Uint8Array). // - err - Returns an error on failure (Error). func (suc *singleUseCallback) Callback(callbackReport []byte, err error) { - suc.callback(utils.CopyBytesToJS(callbackReport), utils.JsTrace(err)) + suc.callback(utils.CopyBytesToJS(callbackReport), exception.NewTrace(err)) } // singleUseResponse wraps Javascript callbacks to adhere to the @@ -145,5 +146,5 @@ type singleUseResponse struct { // (Uint8Array). // - err - Returns an error on failure (Error). func (sur *singleUseResponse) Callback(responseReport []byte, err error) { - sur.callback(utils.CopyBytesToJS(responseReport), utils.JsTrace(err)) + sur.callback(utils.CopyBytesToJS(responseReport), exception.NewTrace(err)) } diff --git a/wasm/timeNow.go b/wasm/timeNow.go index 627a16b4e344089d782edf435fdcd3225c6c230b..ca3ebe6037161df753d80f7ed08e01c4e3d5b335 100644 --- a/wasm/timeNow.go +++ b/wasm/timeNow.go @@ -11,7 +11,7 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) diff --git a/wasm/ud.go b/wasm/ud.go index 74bc2b11b628a4139040168194af9b6d21009a72..cce7c8a7fecc09f6690c42de5891b0910ba3d5f2 100644 --- a/wasm/ud.go +++ b/wasm/ud.go @@ -11,7 +11,8 @@ package wasm import ( "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "syscall/js" ) @@ -99,7 +100,7 @@ func (uns *udNetworkStatus) UdNetworkStatus() int { // Returns: // - Javascript representation of the [UserDiscovery] object that is // registered to the specified UD service. -// - Throws a TypeError if creating or loading fails. +// - Throws an error if creating or loading fails. func NewOrLoadUd(_ js.Value, args []js.Value) any { e2eID := args[0].Int() follower := &udNetworkStatus{utils.WrapCB(args[1], "UdNetworkStatus")} @@ -112,7 +113,7 @@ func NewOrLoadUd(_ js.Value, args []js.Value) any { api, err := bindings.NewOrLoadUd(e2eID, follower, username, registrationValidationSignature, cert, contactFile, address) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -145,7 +146,7 @@ func NewOrLoadUd(_ js.Value, args []js.Value) any { // Returns: // - Javascript representation of the [UserDiscovery] object that is loaded // from backup. -// - Throws a TypeError if getting UD from backup fails. +// - Throws an error if getting UD from backup fails. func NewUdManagerFromBackup(_ js.Value, args []js.Value) any { e2eID := args[0].Int() follower := &udNetworkStatus{utils.WrapCB(args[1], "UdNetworkStatus")} @@ -157,7 +158,7 @@ func NewUdManagerFromBackup(_ js.Value, args []js.Value) any { e2eID, follower, cert, contactFile, address) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -182,7 +183,7 @@ func (ud *UserDiscovery) GetFacts(js.Value, []js.Value) any { func (ud *UserDiscovery) GetContact(js.Value, []js.Value) any { c, err := ud.api.GetContact() if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -203,7 +204,7 @@ func (ud *UserDiscovery) GetContact(js.Value, []js.Value) any { func (ud *UserDiscovery) ConfirmFact(_ js.Value, args []js.Value) any { err := ud.api.ConfirmFact(args[0].String(), args[1].String()) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -228,7 +229,7 @@ func (ud *UserDiscovery) ConfirmFact(_ js.Value, args []js.Value) any { func (ud *UserDiscovery) SendRegisterFact(_ js.Value, args []js.Value) any { confirmationID, err := ud.api.SendRegisterFact(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -247,7 +248,7 @@ func (ud *UserDiscovery) SendRegisterFact(_ js.Value, args []js.Value) any { func (ud *UserDiscovery) PermanentDeleteAccount(_ js.Value, args []js.Value) any { err := ud.api.PermanentDeleteAccount(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -265,7 +266,7 @@ func (ud *UserDiscovery) PermanentDeleteAccount(_ js.Value, args []js.Value) any func (ud *UserDiscovery) RemoveFact(_ js.Value, args []js.Value) any { err := ud.api.RemoveFact(utils.CopyBytesToGo(args[0])) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -290,7 +291,7 @@ type udLookupCallback struct { // the lookup, or nil if an error occurs (Uint8Array). // - err - Returns an error on failure (Error). func (ulc *udLookupCallback) Callback(contactBytes []byte, err error) { - ulc.callback(utils.CopyBytesToJS(contactBytes), utils.JsTrace(err)) + ulc.callback(utils.CopyBytesToJS(contactBytes), exception.NewTrace(err)) } // LookupUD returns the public key of the passed ID as known by the user @@ -322,7 +323,7 @@ func LookupUD(_ js.Value, args []js.Value) any { sendReport, err := bindings.LookupUD( e2eID, udContact, cb, lookupId, singleRequestParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -357,7 +358,7 @@ type udSearchCallback struct { // "<xxc(2)d7RJTu61Vy1lDThDMn8rYIiKSe1uXA/RCvvcIhq5Yg4DEgB7Ugdw/BAr6RsCABkWAFV1c2VybmFtZTI7N3XWrxIUpR29atpFMkcR6A==xxc>" // } func (usc *udSearchCallback) Callback(contactListJSON []byte, err error) { - usc.callback(utils.CopyBytesToJS(contactListJSON), utils.JsTrace(err)) + usc.callback(utils.CopyBytesToJS(contactListJSON), exception.NewTrace(err)) } // SearchUD searches user discovery for the passed Facts. The searchCallback @@ -389,7 +390,7 @@ func SearchUD(_ js.Value, args []js.Value) any { sendReport, err := bindings.SearchUD( e2eID, udContact, cb, factListJSON, singleRequestParamsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } diff --git a/wasm/version.go b/wasm/version.go index ee1a921cc666ef3a5e2491b5f43371ee098e2f3f..048c49b585eb3196d8e1c3926cb87a6c7bf9556e 100644 --- a/wasm/version.go +++ b/wasm/version.go @@ -14,8 +14,9 @@ import ( "syscall/js" "gitlab.com/elixxir/client/v4/bindings" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" "gitlab.com/elixxir/xxdk-wasm/storage" - "gitlab.com/elixxir/xxdk-wasm/utils" ) // GetVersion returns the current xxDK WASM semantic version. @@ -66,7 +67,7 @@ type VersionInfo struct { // // Returns: // - JSON of [VersionInfo] (Uint8Array). -// - Throws a TypeError if getting the version failed. +// - Throws an error if getting the version failed. func GetWasmSemanticVersion(js.Value, []js.Value) any { vi := VersionInfo{ Current: storage.SEMVER, @@ -80,7 +81,7 @@ func GetWasmSemanticVersion(js.Value, []js.Value) any { data, err := json.Marshal(vi) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) } return utils.CopyBytesToJS(data) @@ -91,7 +92,7 @@ func GetWasmSemanticVersion(js.Value, []js.Value) any { // // Returns: // - JSON of [VersionInfo] (Uint8Array). -// - Throws a TypeError if getting the version failed. +// - Throws an error if getting the version failed. func GetXXDKSemanticVersion(js.Value, []js.Value) any { vi := VersionInfo{ Current: bindings.GetVersion(), @@ -104,7 +105,7 @@ func GetXXDKSemanticVersion(js.Value, []js.Value) any { data, err := json.Marshal(vi) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) } return utils.CopyBytesToJS(data) diff --git a/wasm_exec.js b/wasm_exec.js index c6ae00c713c4bb089eb2d76d61d3e88aa0835839..5365a841cd2ff018149c25666e4b525a42bc1344 100644 --- a/wasm_exec.js +++ b/wasm_exec.js @@ -453,7 +453,7 @@ }, // func Throw(exception string, message string) - 'gitlab.com/elixxir/xxdk-wasm/utils.throw': (sp) => { + 'gitlab.com/elixxir/wasm-utils/exception.throw': (sp) => { const exception = loadString(sp + 8) const message = loadString(sp + 24) throw globalThis[exception](message) diff --git a/wasm_test.go b/wasm_test.go index 2d36f7b0c8d3ae5a024ea837f6da9526dc42d3c8..b415ece5623aa4e235511fb1f9cb24058e02e384 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -63,6 +63,12 @@ func TestPublicFunctions(t *testing.T) { // C-Library specific bindings not needed by the browser "GetDMInstance": {}, "GetCMixInstance": {}, + + // Logging has been moved to startup flags + "LogLevel": {}, + + // NewFilesystemRemoteStorage is internal for bindings. + "NewFileSystemRemoteStorage": {}, } wasmFuncs := getPublicFunctions("wasm", t) bindingsFuncs := getPublicFunctions( diff --git a/worker/README.md b/worker/README.md index e5027c3fe7d366d5096bb9b09406ea6ab6edaa2f..18d21f12fb5115a92941451c2f9aa0667ca3fa9a 100644 --- a/worker/README.md +++ b/worker/README.md @@ -18,7 +18,7 @@ package main import ( "fmt" - "gitlab.com/elixxir/xxdk-wasm/utils/worker" + "gitlab.com/elixxir/wasm-utils/utils/worker" ) func main() { @@ -51,4 +51,4 @@ wm, err := worker.NewManager("workerWasm.js", "exampleWebWorker") if err != nil { return nil, err } -``` \ No newline at end of file +``` diff --git a/worker/manager.go b/worker/manager.go index c38438facdf8b502d8b2fc00ba0fe4855b5cfaf8..2809a40922609923f300593a3120082afc46f5a5 100644 --- a/worker/manager.go +++ b/worker/manager.go @@ -18,7 +18,7 @@ import ( "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/utils" ) // initID is the ID for the first item in the callback list. If the list only @@ -41,7 +41,7 @@ const ( // put on. const receiveQueueChanSize = 100 -// ReceptionCallback is the function that handles incoming data from the worker. +// ReceptionCallback is called with a message received from the worker. type ReceptionCallback func(data []byte) // Manager manages the handling of messages received from the worker. @@ -64,7 +64,7 @@ type Manager struct { // receiveQueue is the channel that all received messages are queued on // while they wait to be processed. - receiveQueue chan []byte + receiveQueue chan js.Value // quit, when triggered, stops the thread that processes received messages. quit chan struct{} @@ -89,7 +89,7 @@ func NewManager(aURL, name string, messageLogging bool) (*Manager, error) { worker: js.Global().Get("Worker").New(aURL, opts), callbacks: make(map[Tag]map[uint64]ReceptionCallback), responseIDs: make(map[Tag]uint64), - receiveQueue: make(chan []byte, receiveQueueChanSize), + receiveQueue: make(chan js.Value, receiveQueueChanSize), quit: make(chan struct{}), name: name, messageLogging: messageLogging, @@ -138,11 +138,24 @@ func (m *Manager) processThread() { case <-m.quit: jww.INFO.Printf("[WW] [%s] Quitting process thread.", m.name) return - case message := <-m.receiveQueue: - err := m.processReceivedMessage(message) - if err != nil { - jww.ERROR.Printf("[WW] [%s] Failed to process received "+ - "message from worker: %+v", m.name, err) + case msgData := <-m.receiveQueue: + + switch msgData.Type() { + case js.TypeObject: + if msgData.Get("constructor").Equal(utils.Uint8Array) { + err := m.processReceivedMessage(utils.CopyBytesToGo(msgData)) + if err != nil { + jww.ERROR.Printf("[WW] [%s] Failed to process received "+ + "message from worker: %+v", m.name, err) + } + break + } + fallthrough + + default: + jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+ + "from worker: %s", m.name, msgData.Type(), + utils.JsToJson(msgData)) } } } @@ -174,12 +187,12 @@ func (m *Manager) SendMessage( "ID %d going to worker: %+v", m.name, msg, tag, id, err) } - go m.postMessage(string(payload)) + go m.postMessage(payload) } // receiveMessage is registered with the Javascript event listener and is called // every time a new message from the worker is received. -func (m *Manager) receiveMessage(data []byte) { +func (m *Manager) receiveMessage(data js.Value) { m.receiveQueue <- data } @@ -303,7 +316,7 @@ func (m *Manager) addEventListeners() { // occurs when a message is received from the worker. // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { - m.receiveMessage([]byte(args[0].Get("data").String())) + m.receiveMessage(args[0].Get("data")) return nil }) @@ -312,8 +325,8 @@ func (m *Manager) addEventListeners() { // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { event := args[0] - jww.ERROR.Printf("[WW] [%s] Main received error event: %s", - m.name, utils.JsErrorToJson(event)) + jww.FATAL.Panicf("[WW] [%s] Main received error event: %+v", + m.name, js.Error{Value: event}) return nil }) @@ -322,8 +335,8 @@ func (m *Manager) addEventListeners() { // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { event := args[0] - jww.ERROR.Printf("[WW] [%s] Main received message error event: %s", - m.name, utils.JsErrorToJson(event)) + jww.ERROR.Printf("[WW] [%s] Main received message error event: %+v", + m.name, js.Error{Value: event}) return nil }) @@ -336,20 +349,19 @@ func (m *Manager) addEventListeners() { // postMessage sends a message to the worker. // -// message is the object to deliver to the worker; this will be in the data -// field in the event delivered to the worker. It must be a js.Value or a -// primitive type that can be converted via js.ValueOf. The Javascript object -// must be "any value or JavaScript object handled by the structured clone -// algorithm, which includes cyclical references.". See the doc for more -// information. +// msg is the object to deliver to the worker; this will be in the data +// field in the event delivered to the worker. It must be a transferable object +// because this function transfers ownership of the message instead of copying +// it for better performance. See the doc for more information. // // If the message parameter is not provided, a SyntaxError will be thrown by the // parser. If the data to be passed to the worker is unimportant, js.Null or // js.Undefined can be passed explicitly. // // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage -func (m *Manager) postMessage(msg any) { - m.worker.Call("postMessage", msg) +func (m *Manager) postMessage(msg []byte) { + buffer := utils.CopyBytesToJS(msg) + m.worker.Call("postMessage", buffer, []any{buffer.Get("buffer")}) } // terminate immediately terminates the Worker. This does not offer the worker diff --git a/worker/manager_test.go b/worker/manager_test.go index 49a395a9c45a33f25892cc1efc86e263ccf06063..b071c7e437d149eb49aec41da593f858c4c01354 100644 --- a/worker/manager_test.go +++ b/worker/manager_test.go @@ -21,7 +21,7 @@ func TestManager_processReceivedMessage(t *testing.T) { m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)} msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}) + cbChan := make(chan struct{}, 1) cb := func([]byte) { cbChan <- struct{}{} } m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb} @@ -31,16 +31,16 @@ func TestManager_processReceivedMessage(t *testing.T) { } go func() { - select { - case <-cbChan: - case <-time.After(10 * time.Millisecond): - t.Error("Timed out waiting for callback to be called.") + err = m.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) } }() - err = m.processReceivedMessage(data) - if err != nil { - t.Errorf("Failed to receive message: %+v", err) + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") } } @@ -97,7 +97,7 @@ func TestManager_RegisterCallback(t *testing.T) { m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)} msg := Message{Tag: readyTag, ID: initID} - cbChan := make(chan struct{}) + cbChan := make(chan struct{}, 1) cb := func([]byte) { cbChan <- struct{}{} } m.RegisterCallback(msg.Tag, cb) @@ -107,16 +107,16 @@ func TestManager_RegisterCallback(t *testing.T) { } go func() { - select { - case <-cbChan: - case <-time.After(10 * time.Millisecond): - t.Error("Timed out waiting for callback to be called.") + err = m.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) } }() - err = m.processReceivedMessage(data) - if err != nil { - t.Errorf("Failed to receive message: %+v", err) + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") } } @@ -129,7 +129,7 @@ func TestManager_registerReplyCallback(t *testing.T) { } msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}) + cbChan := make(chan struct{}, 1) cb := func([]byte) { cbChan <- struct{}{} } m.registerReplyCallback(msg.Tag, cb) m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb} @@ -140,16 +140,16 @@ func TestManager_registerReplyCallback(t *testing.T) { } go func() { - select { - case <-cbChan: - case <-time.After(10 * time.Millisecond): - t.Error("Timed out waiting for callback to be called.") + err = m.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) } }() - err = m.processReceivedMessage(data) - if err != nil { - t.Errorf("Failed to receive message: %+v", err) + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") } } diff --git a/worker/thread.go b/worker/thread.go index 07591fc7d8f7a905f521b98215096b2d8d5ba044..393ff7661db3f0c773932fdff932a15a3bfe92b9 100644 --- a/worker/thread.go +++ b/worker/thread.go @@ -17,11 +17,12 @@ import ( "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/utils" ) -// ThreadReceptionCallback is the function that handles incoming data from the -// main thread. +// ThreadReceptionCallback is called with a message received from the main +// thread. Any bytes returned are sent as a response back to the main thread. +// Any returned errors are printed to the log. type ThreadReceptionCallback func(data []byte) ([]byte, error) // ThreadManager queues incoming messages from the main thread and handles them @@ -34,9 +35,9 @@ type ThreadManager struct { // main thread keyed on the callback tag. callbacks map[Tag]ThreadReceptionCallback - // receiveQueue is the channel that all received messages are queued on - // while they wait to be processed. - receiveQueue chan []byte + // receiveQueue is the channel that all received MessageEvent.data are + // queued on while they wait to be processed. + receiveQueue chan js.Value // quit, when triggered, stops the thread that processes received messages. quit chan struct{} @@ -56,7 +57,7 @@ func NewThreadManager(name string, messageLogging bool) *ThreadManager { tm := &ThreadManager{ messages: make(chan js.Value, 100), callbacks: make(map[Tag]ThreadReceptionCallback), - receiveQueue: make(chan []byte, receiveQueueChanSize), + receiveQueue: make(chan js.Value, receiveQueueChanSize), quit: make(chan struct{}), name: name, messageLogging: messageLogging, @@ -88,14 +89,24 @@ func (tm *ThreadManager) processThread() { case <-tm.quit: jww.INFO.Printf("[WW] [%s] Quitting worker process thread.", tm.name) return - case message := <-tm.receiveQueue: - if tm.messageLogging { - jww.INFO.Printf("[WW] Worker processors received message: %q", message) - } - err := tm.processReceivedMessage(message) - if err != nil { - jww.ERROR.Printf("[WW] [%s] Failed to receive message from "+ - "main thread: %+v", tm.name, err) + case msgData := <-tm.receiveQueue: + + switch msgData.Type() { + case js.TypeObject: + if msgData.Get("constructor").Equal(utils.Uint8Array) { + err := tm.processReceivedMessage(utils.CopyBytesToGo(msgData)) + if err != nil { + jww.ERROR.Printf("[WW] [%s] Failed to process message "+ + "received from main thread: %+v", tm.name, err) + } + break + } + fallthrough + + default: + jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+ + "from main thread: %s", + tm.name, msgData.Type(), utils.JsToJson(msgData)) } } } @@ -128,7 +139,7 @@ func (tm *ThreadManager) SendMessage(tag Tag, data []byte) { "to main: %+v", tm.name, msg, tag, err) } - go tm.postMessage(string(payload)) + go tm.postMessage(payload) } // sendResponse sends a reply to the main thread with the given tag and ID. @@ -151,14 +162,14 @@ func (tm *ThreadManager) sendResponse(tag Tag, id uint64, data []byte) error { "%d going to main: %+v", msg, tag, id, err) } - go tm.postMessage(string(payload)) + go tm.postMessage(payload) return nil } // receiveMessage is registered with the Javascript event listener and is called // every time a new message from the main thread is received. -func (tm *ThreadManager) receiveMessage(data []byte) { +func (tm *ThreadManager) receiveMessage(data js.Value) { tm.receiveQueue <- data } @@ -224,7 +235,7 @@ func (tm *ThreadManager) addEventListeners() { // occurs when a message is received from the main thread. // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/message_event messageEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { - tm.receiveMessage([]byte(args[0].Get("data").String())) + tm.receiveMessage(args[0].Get("data")) return nil }) @@ -233,8 +244,8 @@ func (tm *ThreadManager) addEventListeners() { // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/error_event errorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { event := args[0] - jww.ERROR.Printf("[WW] [%s] Worker received error event: %s", - tm.name, utils.JsErrorToJson(event)) + jww.ERROR.Printf("[WW] [%s] Worker received error event: %+v", + tm.name, js.Error{Value: event}) return nil }) @@ -243,8 +254,8 @@ func (tm *ThreadManager) addEventListeners() { // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/messageerror_event messageerrorEvent := js.FuncOf(func(_ js.Value, args []js.Value) any { event := args[0] - jww.ERROR.Printf("[WW] [%s] Worker received message error event: %s", - tm.name, utils.JsErrorToJson(event)) + jww.ERROR.Printf("[WW] [%s] Worker received message error event: %+v", + tm.name, js.Error{Value: event}) return nil }) @@ -260,10 +271,16 @@ func (tm *ThreadManager) addEventListeners() { // aMessage must be a js.Value or a primitive type that can be converted via // js.ValueOf. The Javascript object must be "any value or JavaScript object // handled by the structured clone algorithm". See the doc for more information. + +// aMessage is the object to deliver to the main thread; this will be in the +// data field in the event delivered to the thread. It must be a transferable +// object because this function transfers ownership of the message instead of +// copying it for better performance. See the doc for more information. // // Doc: https://developer.mozilla.org/docs/Web/API/DedicatedWorkerGlobalScope/postMessage -func (tm *ThreadManager) postMessage(aMessage any) { - js.Global().Call("postMessage", aMessage) +func (tm *ThreadManager) postMessage(aMessage []byte) { + buffer := utils.CopyBytesToJS(aMessage) + js.Global().Call("postMessage", buffer, []any{buffer.Get("buffer")}) } // close discards any tasks queued in the worker's event loop, effectively diff --git a/worker/thread_test.go b/worker/thread_test.go index ada6de8fc00916699b45eaee1483830a277b9fc9..d738580aad5d66ad4eab81ee934db52b24313825 100644 --- a/worker/thread_test.go +++ b/worker/thread_test.go @@ -20,7 +20,7 @@ func TestThreadManager_processReceivedMessage(t *testing.T) { tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)} msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}) + cbChan := make(chan struct{}, 1) cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil } tm.callbacks[msg.Tag] = cb @@ -30,16 +30,16 @@ func TestThreadManager_processReceivedMessage(t *testing.T) { } go func() { - select { - case <-cbChan: - case <-time.After(10 * time.Millisecond): - t.Error("Timed out waiting for callback to be called.") + err = tm.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) } }() - err = tm.processReceivedMessage(data) - if err != nil { - t.Errorf("Failed to receive message: %+v", err) + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") } } @@ -49,7 +49,7 @@ func TestThreadManager_RegisterCallback(t *testing.T) { tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)} msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}) + cbChan := make(chan struct{}, 1) cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil } tm.RegisterCallback(msg.Tag, cb) @@ -59,15 +59,15 @@ func TestThreadManager_RegisterCallback(t *testing.T) { } go func() { - select { - case <-cbChan: - case <-time.After(10 * time.Millisecond): - t.Error("Timed out waiting for callback to be called.") + err = tm.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) } }() - err = tm.processReceivedMessage(data) - if err != nil { - t.Errorf("Failed to receive message: %+v", err) + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") } }