diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7ab520cd02563b548eabe925202ef87324725e2c..f70c6360c2609120af3b32b7fad230c47e90d07e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,11 +38,11 @@ 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" + - 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: @@ -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..1567103e23775c56f6d6139b777d2e2274638ab8 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ 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 @@ -18,11 +19,12 @@ update_release: GOFLAGS="" go get -d gitlab.com/elixxir/client/v4@release 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 a48cbe4930867d8decbb48e2c2204ead5b11122e..a9ae4bdb98fe41ff0246255ae5d6fe5e44132a41 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,32 @@ module gitlab.com/elixxir/xxdk-wasm go 1.19 require ( + github.com/aquilax/truncate v1.0.0 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/hack-pad/go-indexeddb v0.2.0 + github.com/hack-pad/safejs v0.1.1 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.20230726212659-ac910c86f8ea - gitlab.com/elixxir/crypto v0.0.7-0.20230512203519-3aad22b6413b - gitlab.com/elixxir/primitives v0.0.3-0.20230724190035-efb1f377c08a - gitlab.com/xx_network/crypto v0.0.5-0.20230724190222-a1fd6f70e6cb - gitlab.com/xx_network/primitives v0.0.4-0.20230724185812-bc6fc6e5341b + github.com/stretchr/testify v1.8.2 + gitlab.com/elixxir/client/v4 v4.6.4-0.20230724165849-04fc01e5b706 + gitlab.com/elixxir/crypto v0.0.7-0.20230614183801-387e0cb8e76f + gitlab.com/elixxir/primitives v0.0.3-0.20230613193928-8cf8bdd777ef + gitlab.com/elixxir/wasm-utils v0.0.0-20230615222914-185dd3a6fa08 + gitlab.com/xx_network/crypto v0.0.5-0.20230214003943-8a09396e95dd + gitlab.com/xx_network/primitives v0.0.4-0.20230522171102-940cdd68e516 golang.org/x/crypto v0.5.0 ) require ( filippo.io/edwards25519 v1.0.0 // indirect git.xx.network/elixxir/grpc-web-go-client v0.0.0-20230214175953-5b5a8c33d28a // indirect + github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect 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 @@ -38,6 +44,7 @@ require ( 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 @@ -49,6 +56,7 @@ 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 @@ -63,8 +71,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.20230310205528-f06faa0d2f0b // indirect - gitlab.com/elixxir/ekv v0.2.2 // indirect + gitlab.com/elixxir/comms v0.0.4-0.20230718154315-08043221466a // indirect + gitlab.com/elixxir/ekv v0.3.1-0.20230620180825-838848b00f19 // 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 @@ -73,7 +81,7 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect golang.org/x/net v0.5.0 // indirect - golang.org/x/sys v0.10.0 // indirect + golang.org/x/sys v0.4.0 // indirect golang.org/x/text v0.6.0 // indirect google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect google.golang.org/grpc v1.49.0 // indirect diff --git a/go.sum b/go.sum index ac82ec52357b833fe7ea0e6f93daf6ac45f2fb8c..482c8d0854c09850ee533b6db2552c8ddd7c4b74 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ git.xx.network/elixxir/grpc-web-go-client v0.0.0-20230214175953-5b5a8c33d28a/go. github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE= +github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= @@ -56,6 +58,8 @@ github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9or github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/aquilax/truncate v1.0.0 h1:UgIGS8U/aZ4JyOJ2h3xcF5cSQ06+gGBnjxH2RUHJe0U= +github.com/aquilax/truncate v1.0.0/go.mod h1:BeMESIDMlvlS3bmg4BVvBbbZUNwWtS8uzYPAKXwwhLw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -97,6 +101,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= @@ -238,6 +243,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hack-pad/go-indexeddb v0.2.0 h1:QHDM6gLrtCJvHdHUK8UdibJu4xWQlIDs4+l8L65AUdA= github.com/hack-pad/go-indexeddb v0.2.0/go.mod h1:NH8CaojufPNcKYDhy5JkjfyBXE/72oJPeiywlabN/lM= +github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8= +github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -304,8 +311,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= @@ -423,6 +434,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= @@ -506,24 +519,30 @@ 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.20230726212659-ac910c86f8ea h1:ozegoQF2GZI03+KulCATRy4d3+dcB5gRB7A5DOn+/HU= -gitlab.com/elixxir/client/v4 v4.6.4-0.20230726212659-ac910c86f8ea/go.mod h1:4Xne/cpHWT3rJI1HshYpwzr50scQHKnPSv+dckm9VZo= -gitlab.com/elixxir/comms v0.0.4-0.20230310205528-f06faa0d2f0b h1:8AVK93UEs/aufoqtFgyMVt9gf0oJ8F4pA60ZvEVvG+s= -gitlab.com/elixxir/comms v0.0.4-0.20230310205528-f06faa0d2f0b/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q= -gitlab.com/elixxir/crypto v0.0.7-0.20230512203519-3aad22b6413b h1:+3iFGyrfjEbzIg3hoGJkDJAbTlqaMx1PuOth7HcL2yk= -gitlab.com/elixxir/crypto v0.0.7-0.20230512203519-3aad22b6413b/go.mod h1:/SLOlvkYVVJf6IU+vEjMLnS7cjjcoTlPV45g6tv6INc= -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/primitives v0.0.3-0.20230724190035-efb1f377c08a h1:hPp0oYlRM8GJseskzBb2Xr9zEzQRNjkiQg3n7PfpZ/A= -gitlab.com/elixxir/primitives v0.0.3-0.20230724190035-efb1f377c08a/go.mod h1:0KOMjw62km9QI4Ow/XNJqg4P+gu1OQUGDhHM5gMYGS8= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230630173422-ac9c5506b1fb h1:991u/qqqP2FI3MaH5y7MZE60HsXKXJ4lqCd2dYJcS88= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230630173422-ac9c5506b1fb/go.mod h1:wSeJ9pk+qqUrKHwhd4qZW1CnNlakK75n+1fOjJ7k1Ns= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230717200544-5e105cfcd312 h1:SHiuZDKobnDyL8XPWxfiQnpgPovwv+hTdtx86voIu/U= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230717200544-5e105cfcd312/go.mod h1:wSeJ9pk+qqUrKHwhd4qZW1CnNlakK75n+1fOjJ7k1Ns= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230724165849-04fc01e5b706 h1:cB66N82jdeARHVQBYcqu7IwcyvBlK5zUJMRi7Y3vy08= +gitlab.com/elixxir/client/v4 v4.6.4-0.20230724165849-04fc01e5b706/go.mod h1:taeAIsjcAgdK+HytutNR8jabSSmiHaliCAnQk8UPvnc= +gitlab.com/elixxir/comms v0.0.4-0.20230613220741-7de1d2ca4a1c h1:0TpLn4AdarrqCwUMvnz4Md+9gLyk9wrQ73J3W9U5zJo= +gitlab.com/elixxir/comms v0.0.4-0.20230613220741-7de1d2ca4a1c/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q= +gitlab.com/elixxir/comms v0.0.4-0.20230718154315-08043221466a h1:+cG6UF7WDW5aFX/xOoLa77k765HnKqxT/2LSDd70nKk= +gitlab.com/elixxir/comms v0.0.4-0.20230718154315-08043221466a/go.mod h1:z+qW0D9VpY5QKTd7wRlb5SK4kBNqLYsa4DXBcUXue9Q= +gitlab.com/elixxir/crypto v0.0.7-0.20230614183801-387e0cb8e76f h1:T0Jvhq5nCELiwkVr07Ti/Ew9ICdexviYeCkFV19kk9A= +gitlab.com/elixxir/crypto v0.0.7-0.20230614183801-387e0cb8e76f/go.mod h1:lAib0KO9TeTLWbwgFk2uszRxPkHeu843xqnYdkzdEB0= +gitlab.com/elixxir/ekv v0.3.1-0.20230620180825-838848b00f19 h1:GZLTKoxr6r5QhG5BvZoQeImeXrpnn03EhG8RTxrLyl0= +gitlab.com/elixxir/ekv v0.3.1-0.20230620180825-838848b00f19/go.mod h1:8sVMOf3GFQCnd3z8Nnlg8Udno+Lcc1qNXxleblhzXTA= +gitlab.com/elixxir/primitives v0.0.3-0.20230613193928-8cf8bdd777ef h1:31Uavv2Y1lNnPsxPe8lAsJClf7h6y9c3NrqMIvA9ae8= +gitlab.com/elixxir/primitives v0.0.3-0.20230613193928-8cf8bdd777ef/go.mod h1:phun4PLkHJA6wcL4JIhhxZztrmCyJHWPNppBP3DUD2Y= +gitlab.com/elixxir/wasm-utils v0.0.0-20230615222914-185dd3a6fa08 h1:7Uqf6CHEBj/q1CIHEwnWcYLFG5cpMtH8oyVYGikgYWo= +gitlab.com/elixxir/wasm-utils v0.0.0-20230615222914-185dd3a6fa08/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.20230724190222-a1fd6f70e6cb h1:bGcPR0oeiUgGK264QE5mXzIttjeqQ8+kJaspOuY74dc= -gitlab.com/xx_network/crypto v0.0.5-0.20230724190222-a1fd6f70e6cb/go.mod h1:Q1tnwsNnopT0L35VAtlLizf08///cBR8MFIbuZtEYng= -gitlab.com/xx_network/primitives v0.0.4-0.20230724185812-bc6fc6e5341b h1:oymmRpA+5/SeBp+MgFeYyuB8S9agfetGDnxBXXq9utE= -gitlab.com/xx_network/primitives v0.0.4-0.20230724185812-bc6fc6e5341b/go.mod h1:vI6JXexgqihcIVFGsZAwGlHWGT14XC24NwEB2c6q9nc= +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.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= @@ -729,8 +748,8 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -912,6 +931,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 205a5e0df78b3cb6125042a963e5de8026fd4e08..8eeadabeb2a61236bded58a5abb77d9d263d27a3 100644 --- a/indexedDb/impl/channels/callbacks.go +++ b/indexedDb/impl/channels/callbacks.go @@ -10,21 +10,23 @@ package main import ( - "crypto/ed25519" "encoding/json" + "time" + "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "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/fastRNG" + idbCrypto "gitlab.com/elixxir/crypto/indexedDb" "gitlab.com/elixxir/crypto/message" + "gitlab.com/elixxir/wasm-utils/exception" wChannels "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/channels" "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} @@ -45,8 +47,8 @@ func (m *manager) registerCallbacks() { 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.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) @@ -54,119 +56,100 @@ func (m *manager) registerCallbacks() { // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty // slice on success or an error message on failure. -func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) { +func (m *manager) newWASMEventModelCB(message []byte, reply func(message []byte)) { var msg wChannels.NewWASMEventModelMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return []byte{}, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + reply([]byte(errors.Wrapf(err, + "failed to JSON unmarshal %T from main thread", msg).Error())) + return } // Create new encryption cipher rng := fastRNG.NewStreamGenerator(12, 1024, csprng.NewSystemRNG) - encryption, err := cryptoChannel.NewCipherFromJSON( + encryption, err := idbCrypto.NewCipherFromJSON( []byte(msg.EncryptionJSON), rng.GetStream()) if err != nil { - return []byte{}, errors.Errorf( - "failed to JSON unmarshal Cipher from main thread: %+v", err) + reply([]byte(errors.Wrap(err, + "failed to JSON unmarshal Cipher from main thread").Error())) + return } - m.model, err = NewWASMEventModel(msg.DatabaseName, encryption, - m.messageReceivedCallback, m.deletedMessageCallback, m.mutedUserCallback) + m.model, err = NewWASMEventModel( + msg.DatabaseName, encryption, m.eventUpdateCallback) if err != nil { - return []byte(err.Error()), nil + reply([]byte(err.Error())) + return } - return []byte{}, nil + reply(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) +// eventUpdateCallback JSON marshals the interface and sends it to the main +// thread the with the event type to be sent on the EventUpdate callback. +func (m *manager) eventUpdateCallback(eventType int64, jsonMarshallable any) { + jsonData, err := json.Marshal(jsonMarshallable) if err != nil { - jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err) - return + jww.FATAL.Panicf("[CH] Failed to JSON marshal %T for EventUpdate "+ + "callback: %+v", jsonMarshallable, err) } - // Send it to the main thread - m.wtm.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.wtm.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) { // 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 { - jww.ERROR.Printf("Could not JSON marshal %T: %+v", msg, err) - return + exception.Throwf("[CH] Could not JSON marshal %T for EventUpdate "+ + "callback: %+v", msg, err) } // Send it to the main thread - m.wtm.SendMessage(wChannels.MutedUserCallbackTag, data) + err = m.wtm.SendNoResponse(wChannels.EventUpdateCallbackTag, data) + if err != nil { + exception.Throwf( + "[CH] Could not send message for EventUpdate callback: %+v", err) + } } // joinChannelCB is the callback for wasmModel.JoinChannel. Always returns nil; // meaning, no response is supplied (or expected). -func (m *manager) joinChannelCB(data []byte) ([]byte, error) { +func (m *manager) joinChannelCB(message []byte, _ func([]byte)) { var channel cryptoBroadcast.Channel - err := json.Unmarshal(data, &channel) + err := json.Unmarshal(message, &channel) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", channel, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal %T from main thread: "+ + "%+v", channel, err) + return } m.model.JoinChannel(&channel) - return nil, nil } // leaveChannelCB is the callback for wasmModel.LeaveChannel. Always returns // nil; meaning, no response is supplied (or expected). -func (m *manager) leaveChannelCB(data []byte) ([]byte, error) { - channelID, err := id.Unmarshal(data) +func (m *manager) leaveChannelCB(message []byte, _ func([]byte)) { + channelID, err := id.Unmarshal(message) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", channelID, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal %T from main thread: "+ + "%+v", channelID, err) + return } m.model.LeaveChannel(channelID) - return nil, nil } // receiveMessageCB is the callback for wasmModel.ReceiveMessage. Returns a UUID // of 0 on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveMessageCB(data []byte) ([]byte, error) { +func (m *manager) receiveMessageCB(message []byte, reply func(message []byte)) { var msg channels.ModelMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal payload for "+ + "ReceiveMessage from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveMessage(msg.ChannelID, msg.MessageID, msg.Nickname, @@ -174,21 +157,25 @@ func (m *manager) receiveMessageCB(data []byte) ([]byte, error) { msg.Timestamp, msg.Lease, rounds.Round{ID: msg.Round}, msg.Type, msg.Status, msg.Hidden) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[CH] Could not JSON marshal UUID for ReceiveMessage: %+v", err) } - return uuidData, nil + + reply(replyMsg) } // receiveReplyCB is the callback for wasmModel.ReceiveReply. Returns a UUID of // 0 on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveReplyCB(data []byte) ([]byte, error) { +func (m *manager) receiveReplyCB(message []byte, reply func(message []byte)) { var msg wChannels.ReceiveReplyMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal payload for "+ + "ReceiveReply from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveReply(msg.ChannelID, msg.MessageID, msg.ReactionTo, @@ -196,21 +183,25 @@ func (m *manager) receiveReplyCB(data []byte) ([]byte, error) { msg.CodesetVersion, msg.Timestamp, msg.Lease, rounds.Round{ID: msg.Round}, msg.Type, msg.Status, msg.Hidden) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[CH] Could not JSON marshal UUID for ReceiveReply: %+v", err) } - return uuidData, nil + + reply(replyMsg) } // receiveReactionCB is the callback for wasmModel.ReceiveReaction. Returns a // UUID of 0 on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveReactionCB(data []byte) ([]byte, error) { +func (m *manager) receiveReactionCB(message []byte, reply func(message []byte)) { var msg wChannels.ReceiveReplyMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal payload for "+ + "ReceiveReaction from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveReaction(msg.ChannelID, msg.MessageID, @@ -218,22 +209,26 @@ func (m *manager) receiveReactionCB(data []byte) ([]byte, error) { msg.DmToken, msg.CodesetVersion, msg.Timestamp, msg.Lease, rounds.Round{ID: msg.Round}, msg.Type, msg.Status, msg.Hidden) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[CH] Could not JSON marshal UUID for ReceiveReaction: %+v", err) } - return uuidData, nil + + reply(replyMsg) } -// updateFromUUIDCB is the callback for wasmModel.UpdateFromUUID. Always returns +// updateFromUuidCB is the callback for wasmModel.UpdateFromUUID. Always returns // nil; meaning, no response is supplied (or expected). -func (m *manager) updateFromUUIDCB(data []byte) ([]byte, error) { +func (m *manager) updateFromUuidCB(messageData []byte, reply func(message []byte)) { var msg wChannels.MessageUpdateInfo - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(messageData, &msg) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + reply([]byte(errors.Errorf("failed to JSON unmarshal %T from main "+ + "thread: %+v", msg, err).Error())) + return } + var messageID *message.ID var timestamp *time.Time var round *rounds.Round @@ -261,20 +256,31 @@ func (m *manager) updateFromUUIDCB(data []byte) ([]byte, error) { err = m.model.UpdateFromUUID( msg.UUID, messageID, timestamp, round, pinned, hidden, status) if err != nil { - return []byte(err.Error()), nil + reply([]byte(err.Error())) } - return nil, nil + reply(nil) } -// updateFromMessageIDCB is the callback for wasmModel.UpdateFromMessageID. +// updateFromMessageIdCB is the callback for wasmModel.UpdateFromMessageID. // Always returns nil; meaning, no response is supplied (or expected). -func (m *manager) updateFromMessageIDCB(data []byte) ([]byte, error) { +func (m *manager) updateFromMessageIdCB(message []byte, reply func(message []byte)) { + var ue wChannels.UuidError + defer func() { + if replyMessage, err := json.Marshal(ue); err != nil { + exception.Throwf("[CH] Failed to JSON marshal %T for "+ + "UpdateFromMessageID: %+v", ue, err) + } else { + reply(replyMessage) + } + }() + var msg wChannels.MessageUpdateInfo - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + ue.Error = errors.Errorf( + "failed to JSON unmarshal %T from main thread: %+v", msg, err).Error() + return } var timestamp *time.Time var round *rounds.Round @@ -296,79 +302,72 @@ func (m *manager) updateFromMessageIDCB(data []byte) ([]byte, error) { status = &msg.Status } - var ue wChannels.UuidError uuid, err := m.model.UpdateFromMessageID( msg.MessageID, timestamp, round, pinned, hidden, status) if err != nil { - ue.Error = []byte(err.Error()) + ue.Error = err.Error() } else { ue.UUID = uuid } - - data, err = json.Marshal(ue) - if err != nil { - return nil, errors.Errorf("failed to JSON marshal %T: %+v", ue, err) - } - - return data, nil } // getMessageCB is the callback for wasmModel.GetMessage. Returns JSON // marshalled channels.GetMessageMessage. If an error occurs, then Error will // be set with the error message. Otherwise, Message will be set. Only one field // will be set. -func (m *manager) getMessageCB(data []byte) ([]byte, error) { - messageID, err := message.UnmarshalID(data) +func (m *manager) getMessageCB(messageData []byte, reply func(message []byte)) { + var replyMsg wChannels.GetMessageMessage + defer func() { + if replyMessage, err := json.Marshal(replyMsg); err != nil { + exception.Throwf("[CH] Failed to JSON marshal %T for "+ + "GetMessage: %+v", replyMsg, err) + } else { + reply(replyMessage) + } + }() + + messageID, err := message.UnmarshalID(messageData) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", messageID, err) + replyMsg.Error = errors.Errorf("failed to JSON unmarshal %T from "+ + "main thread: %+v", messageID, err).Error() + return } - reply := wChannels.GetMessageMessage{} - msg, err := m.model.GetMessage(messageID) if err != nil { - reply.Error = err.Error() + replyMsg.Error = err.Error() } else { - reply.Message = msg + replyMsg.Message = msg } - - messageData, err := json.Marshal(reply) - if err != nil { - return nil, errors.Errorf("failed to JSON marshal %T from main thread "+ - "for GetMessage reply: %+v", reply, err) - } - return messageData, nil } // deleteMessageCB is the callback for wasmModel.DeleteMessage. Always returns // nil; meaning, no response is supplied (or expected). -func (m *manager) deleteMessageCB(data []byte) ([]byte, error) { - messageID, err := message.UnmarshalID(data) +func (m *manager) deleteMessageCB(messageData []byte, reply func(message []byte)) { + messageID, err := message.UnmarshalID(messageData) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", messageID, err) + reply([]byte(errors.Errorf("failed to JSON unmarshal %T from main "+ + "thread: %+v", messageID, err).Error())) + return } err = m.model.DeleteMessage(messageID) if err != nil { - return []byte(err.Error()), nil + reply([]byte(err.Error())) } - return nil, nil + reply(nil) } // muteUserCB is the callback for wasmModel.MuteUser. Always returns nil; // meaning, no response is supplied (or expected). -func (m *manager) muteUserCB(data []byte) ([]byte, error) { +func (m *manager) muteUserCB(message []byte, _ func([]byte)) { var msg wChannels.MuteUserMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[CH] Could not JSON unmarshal %T for MuteUser from "+ + "main thread: %+v", msg, err) + return } - m.model.MuteUser(msg.ChannelID, msg.PubKey, msg.Unmute) - - return nil, nil } diff --git a/indexedDb/impl/channels/channelsIndexedDbWorker.js b/indexedDb/impl/channels/channelsIndexedDbWorker.js index c109cba89735425f23c0d4d436532a3e3eb9a852..4ca8fa6c5d5910ed80b200efa00f2f2fa7d3073f 100644 --- a/indexedDb/impl/channels/channelsIndexedDbWorker.js +++ b/indexedDb/impl/channels/channelsIndexedDbWorker.js @@ -12,6 +12,10 @@ const isReady = new Promise((resolve) => { }); const go = new Go(); +go.argv = [ + '--logLevel=2', + '--threadLogLevel=2', +] const binPath = 'xxdk-channelsIndexedDkWorker.wasm' WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); 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 0976d263b00011b7ec4a6a5d6bfce38c2780beb0..8980a4bbd337b0993bd4b9963f6d89cb326b10f6 100644 --- a/indexedDb/impl/channels/implementation.go +++ b/indexedDb/impl/channels/implementation.go @@ -12,6 +12,7 @@ package main import ( "crypto/ed25519" "encoding/json" + "gitlab.com/elixxir/client/v4/bindings" "strconv" "strings" "syscall/js" @@ -24,25 +25,20 @@ import ( "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" + idbCrypto "gitlab.com/elixxir/crypto/indexedDb" "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 idbCrypto.Cipher + eventCallback eventUpdate } // JoinChannel is called whenever a channel is joined locally. @@ -149,21 +145,22 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID, nickname, text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 { - textBytes := []byte(text) var err error // Handle encryption, if it is present if w.cipher != nil { - textBytes, err = w.cipher.Encrypt([]byte(text)) + text, err = w.cipher.Encrypt([]byte(text)) if err != nil { jww.ERROR.Printf("Failed to encrypt Message: %+v", err) return 0 } } + channelIDBytes := channelID.Marshal() + msgToInsert := buildMessage( - channelID.Marshal(), messageID.Bytes(), nil, nickname, - textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, + channelIDBytes, messageID.Bytes(), nil, nickname, + text, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, false, hidden, status) uuid, err := w.upsertMessage(msgToInsert) @@ -172,7 +169,11 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID, return 0 } - go w.receivedMessageCB(uuid, channelID, false) + go w.eventCallback(bindings.MessageReceived, bindings.MessageReceivedJSON{ + UUID: int64(uuid), + ChannelID: channelID, + Update: false, + }) return uuid } @@ -187,20 +188,21 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 { - textBytes := []byte(text) var err error // Handle encryption, if it is present if w.cipher != nil { - textBytes, err = w.cipher.Encrypt([]byte(text)) + text, err = w.cipher.Encrypt([]byte(text)) if err != nil { jww.ERROR.Printf("Failed to encrypt Message: %+v", err) return 0 } } - msgToInsert := buildMessage(channelID.Marshal(), messageID.Bytes(), - replyTo.Bytes(), nickname, textBytes, pubKey, dmToken, codeset, + channelIDBytes := channelID.Marshal() + + msgToInsert := buildMessage(channelIDBytes, messageID.Bytes(), + replyTo.Bytes(), nickname, text, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, hidden, false, status) uuid, err := w.upsertMessage(msgToInsert) @@ -208,7 +210,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.eventCallback(bindings.MessageReceived, bindings.MessageReceivedJSON{ + UUID: int64(uuid), + ChannelID: channelID, + Update: false, + }) return uuid } @@ -223,21 +230,21 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round, mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 { - textBytes := []byte(reaction) var err error // Handle encryption, if it is present if w.cipher != nil { - textBytes, err = w.cipher.Encrypt([]byte(reaction)) + reaction, err = w.cipher.Encrypt([]byte(reaction)) if err != nil { jww.ERROR.Printf("Failed to encrypt Message: %+v", err) return 0 } } + channelIDBytes := channelID.Marshal() msgToInsert := buildMessage( - channelID.Marshal(), messageID.Bytes(), reactionTo.Bytes(), nickname, - textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, + channelIDBytes, messageID.Bytes(), reactionTo.Bytes(), nickname, + reaction, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType, false, hidden, status) uuid, err := w.upsertMessage(msgToInsert) @@ -245,7 +252,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.eventCallback(bindings.MessageReceived, bindings.MessageReceivedJSON{ + UUID: int64(uuid), + ChannelID: channelID, + Update: false, + }) return uuid } @@ -334,8 +346,8 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID, // NOTE: ID is not set inside this function because we want to use the // autoincrement key by default. If you are trying to overwrite an existing // message, then you need to set it manually yourself. -func buildMessage(channelID, messageID, parentID []byte, nickname string, - text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, +func buildMessage(channelID, messageID, parentID []byte, nickname, + text string, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration, round id.Round, mType channels.MessageType, pinned, hidden bool, status channels.SentStatus) *Message { @@ -392,9 +404,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.eventCallback(bindings.MessageReceived, bindings.MessageReceivedJSON{ + UUID: int64(uuid), + ChannelID: channelID, + Update: true, + }) return uuid, nil } @@ -415,6 +435,26 @@ func (w *wasmModel) upsertMessage(msg *Message) (uint64, error) { // Store message to database msgIdObj, err := impl.Put(w.db, messageStoreName, messageObj) if err != nil { + // Do not error out when this message already exists inside + // the DB. Instead, set the ID and re-attempt as an update. + if msg.ID == 0 { // always error out when not an insert attempt + msgID, inErr := message.UnmarshalID(msg.MessageID) + if inErr == nil { + jww.WARN.Printf("upsertMessage duplicate: %+v", + err) + rnd := &rounds.Round{ID: id.Round(msg.Round)} + status := (*channels.SentStatus)(&msg.Status) + return w.UpdateFromMessageID(msgID, + &msg.Timestamp, + rnd, + &msg.Pinned, + &msg.Hidden, + status) + } + // Add this to the main putMessage error + err = errors.Wrapf(err, "bad msg ID: %+v", + inErr) + } return 0, errors.Errorf("Unable to put Message: %+v\n%s", err, newMessageJson) } @@ -476,7 +516,7 @@ func (w *wasmModel) GetMessage( Status: channels.SentStatus(lookupResult.Status), Hidden: lookupResult.Hidden, Pinned: lookupResult.Pinned, - Content: lookupResult.Text, + Content: []byte(lookupResult.Text), Type: channels.MessageType(lookupResult.Type), Round: id.Round(lookupResult.Round), PubKey: lookupResult.Pubkey, @@ -492,7 +532,9 @@ func (w *wasmModel) DeleteMessage(messageID message.ID) error { return err } - go w.deletedMessageCB(messageID) + go w.eventCallback(bindings.MessageDeleted, bindings.MessageDeletedJSON{ + MessageID: messageID, + }) return nil } @@ -500,7 +542,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.eventCallback(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..4f52e0ff707de1a611384c41e816613ebd39638a 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" + idbCrypto "gitlab.com/elixxir/crypto/indexedDb" "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,12 @@ 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) {} +var dummyEU = func(int64, any) {} // 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, dummyEU) if err != nil { t.Fatal(err) } @@ -121,12 +118,12 @@ func TestWasmModel_ReceiveFile(t *testing.T) { // Happy path, insert message and look it up func TestWasmModel_GetMessage(t *testing.T) { - cipher, err := cryptoChannel.NewCipher( + cipher, err := idbCrypto.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } - for _, c := range []cryptoChannel.Cipher{nil, cipher} { + for _, c := range []idbCrypto.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" @@ -136,14 +133,13 @@ func TestWasmModel_GetMessage(t *testing.T) { storage.GetLocalStorage().Clear() testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) - eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + eventModel, err := newWASMModel(testString, c, dummyEU) if err != nil { t.Fatal(err) } testMsg := buildMessage(id.NewIdFromBytes([]byte(testString), t).Marshal(), - testMsgId.Bytes(), nil, testString, []byte(testString), + testMsgId.Bytes(), nil, testString, testString, []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) _, err = eventModel.upsertMessage(testMsg) @@ -167,15 +163,14 @@ 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, dummyEU) if err != nil { t.Fatal(err) } // Insert a message testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), + testString, testString, []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) _, err = eventModel.upsertMessage(testMsg) if err != nil { @@ -209,12 +204,12 @@ func TestWasmModel_DeleteMessage(t *testing.T) { // Test wasmModel.UpdateSentStatus happy path and ensure fields don't change. func Test_wasmModel_UpdateSentStatus(t *testing.T) { - cipher, err := cryptoChannel.NewCipher( + cipher, err := idbCrypto.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } - for _, c := range []cryptoChannel.Cipher{nil, cipher} { + for _, c := range []idbCrypto.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" @@ -224,15 +219,18 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { testString := "Test_wasmModel_UpdateSentStatus" + cs testMsgId := message.DeriveChannelMessageID( &id.ID{1}, 0, []byte(testString)) - eventModel, err2 := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + eventModel, err2 := newWASMModel(testString, c, dummyEU) 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, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, + testMsg := buildMessage(cid.Bytes(), testMsgId.Bytes(), nil, + testString, testString, []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) uuid, err2 := eventModel.upsertMessage(testMsg) if err2 != nil { @@ -280,20 +278,19 @@ func Test_wasmModel_UpdateSentStatus(t *testing.T) { // Smoke test wasmModel.JoinChannel/wasmModel.LeaveChannel happy paths. func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { - cipher, err := cryptoChannel.NewCipher( + cipher, err := idbCrypto.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } - for _, c := range []cryptoChannel.Cipher{nil, cipher} { + for _, c := range []idbCrypto.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" } 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, dummyEU) if err2 != nil { t.Fatal(err2) } @@ -333,12 +330,12 @@ func Test_wasmModel_JoinChannel_LeaveChannel(t *testing.T) { // Test UUID gets returned when different messages are added. func Test_wasmModel_UUIDTest(t *testing.T) { - cipher, err := cryptoChannel.NewCipher( + cipher, err := idbCrypto.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } - for _, c := range []cryptoChannel.Cipher{nil, cipher} { + for _, c := range []idbCrypto.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" @@ -346,8 +343,7 @@ func Test_wasmModel_UUIDTest(t *testing.T) { t.Run("Test_wasmModel_UUIDTest"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() testString := "testHello" + cs - eventModel, err2 := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + eventModel, err2 := newWASMModel(testString, c, dummyEU) if err2 != nil { t.Fatal(err2) } @@ -380,12 +376,12 @@ func Test_wasmModel_UUIDTest(t *testing.T) { // Tests if the same message ID being sent always returns the same UUID. func Test_wasmModel_DuplicateReceives(t *testing.T) { - cipher, err := cryptoChannel.NewCipher( + cipher, err := idbCrypto.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } - for _, c := range []cryptoChannel.Cipher{nil, cipher} { + for _, c := range []idbCrypto.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" @@ -393,8 +389,7 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) { testString := "Test_wasmModel_DuplicateReceives" + cs t.Run(testString, func(t *testing.T) { storage.GetLocalStorage().Clear() - eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + eventModel, err := newWASMModel(testString, c, dummyEU) if err != nil { t.Fatal(err) } @@ -412,13 +407,16 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) { } // Store duplicate messages with same messageID + referenceID := uint64(0) for i := 0; i < 10; i++ { uuid = eventModel.ReceiveMessage(channelID, msgID, "test", testString+fmt.Sprintf("%d", i), []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Hour, rnd, 0, channels.Sent, false) - if uuid != 0 { - t.Fatalf("Expected UUID to be zero for duplicate receives") + if referenceID == 0 { + referenceID = uuid } + require.Equal(t, referenceID, uuid, + "UUID must be identical for duplicate receives") } }) } @@ -427,12 +425,12 @@ func Test_wasmModel_DuplicateReceives(t *testing.T) { // Happy path: Inserts many messages, deletes some, and checks that the final // result is as expected. func Test_wasmModel_deleteMsgByChannel(t *testing.T) { - cipher, err := cryptoChannel.NewCipher( + cipher, err := idbCrypto.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } - for _, c := range []cryptoChannel.Cipher{nil, cipher} { + for _, c := range []idbCrypto.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" @@ -442,8 +440,7 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { storage.GetLocalStorage().Clear() totalMessages := 10 expectedMessages := 5 - eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + eventModel, err := newWASMModel(testString, c, dummyEU) if err != nil { t.Fatal(err) } @@ -500,12 +497,12 @@ func Test_wasmModel_deleteMsgByChannel(t *testing.T) { // This test is designed to prove the behavior of unique indexes. // Inserts will not fail, they simply will not happen. func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { - cipher, err := cryptoChannel.NewCipher( + cipher, err := idbCrypto.NewCipher( []byte("testPass"), []byte("testSalt"), 128, csprng.NewSystemRNG()) if err != nil { t.Fatalf("Failed to create cipher") } - for i, c := range []cryptoChannel.Cipher{nil, cipher} { + for i, c := range []idbCrypto.Cipher{nil, cipher} { cs := "" if c != nil { cs = "_withCipher" @@ -513,8 +510,7 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { t.Run("TestWasmModel_receiveHelper_UniqueIndex"+cs, func(t *testing.T) { storage.GetLocalStorage().Clear() testString := fmt.Sprintf("test_receiveHelper_UniqueIndex_%d", i) - eventModel, err := newWASMModel(testString, c, - dummyReceivedMessageCB, dummyDeletedMessageCB, dummyMutedUserCB) + eventModel, err := newWASMModel(testString, c, dummyEU) if err != nil { t.Fatal(err) } @@ -541,12 +537,12 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, []byte(testString)) testMsg := buildMessage([]byte(testString), testMsgId.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, + testString, testString, []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) testMsgId2 := message.DeriveChannelMessageID(&id.ID{2}, 0, []byte(testString)) testMsg2 := buildMessage([]byte(testString), testMsgId2.Bytes(), nil, - testString, []byte(testString), []byte{8, 6, 7, 5}, 0, 0, + testString, testString, []byte{8, 6, 7, 5}, 0, 0, netTime.Now(), time.Second, 0, 0, false, false, channels.Sent) // First message insert should succeed @@ -586,3 +582,67 @@ func TestWasmModel_receiveHelper_UniqueIndex(t *testing.T) { }) } } + +func TestUpsertMessageDuplicates(t *testing.T) { + msgJSON := []byte(`{"pubkey":"4gFD38jRhf/aNDRzaguLJHHgTncs0pwvNiRvjTBOaXk=", + "dm_token":3173028031, + "nickname":"", + "message_id":"BTi+V2anHhrRAEcCMm6g2Bs9oKn7PhQzCzvM7x8Ml7A=", + "channel_id":"Gt1T8t1GcnQXijiOjX5yjiLt3F8tnPB9S70ntBx86h8D", + "parent_message_id":null, + "pinned":false, + "hidden":false, + "round":151369004, + "codeset_version":0, + "timestamp": "2023-06-06T19:39:27.281204113Z", + "lease_v2":"30000000000", + "status":2, + "type":1, + "text":"KzT25s76K9AEIv0HadhjULkU37dkCUp/8N9+5UkkA8pLEBK71P7KFMr2xii/enYr4jpQcayNm82WD4wisimkK2wtaKb0hJIELAxJHxHilF2qVcDYn7zmx2ZiYkerZGTlf7DkHPZrA+EJJIu9gZ4ApR8TNVf15GxPSDGKHV6iv7vEMrrdoi4JKOFTpNsUjjbDykc91ADmmRlh5LUR/lqyt7bEasyVY2Zo+UiZpJrUJII0fhhn0HLbfs6ekg1tkq+vbPap3vNoebLZag3nEnMk+JqNjVW93BFICSMFv8cYX9q0sSK6sgHtz0HX9b7xTEFVHlCb2Zv/ErOX2LiedsgM4aGpOvjGYQM9ludCutVJArfM/5ejupMpvVPimY69QdevqxMm6v7c8H3abLQf4iKNVoaTaRygYFYArTWWoKep7BDZhjhCd4XnILGLjq7oeBfb3enMUSmeYabIuQpjAfRXU6cZh4cVomhEbkF5kj/rAc3GLzWtN1XcE5PC8bTL3m6YVN6hMCd1S7dsnDQ4FJQoDoqYzmTLD7jtyQujS86JsPkkt7kAIi14VxHTNEAM07cN6rH4VifWWMgQo4djPJ+nnsW9zUnylT9KS1GGyeaUhGC7GmrJGd7DmfHrhdWb9iMboKyH7VTm5C6X71JW1AhmdFfjtIHmWfhbAFsrP2gwkb4e+F86urUmK5LTT6qhGklUsXsmMFjwiEUCBWLSOZLKfITkNOBGIuGGcMi8Jtvwo1f4DHQ6sm/Y8qaJFmesyVKpA3MAVNawqlZJSJXflUA/mpJprFxLm/J3mdnxkLrqQSeSCAxHgpEdH3jiSPNhmn6QaJOVT+wotm+8lAq7qWgBWfMmcVSgWdAfVwujeP71Uo1y24/Z4+jXulFz1hPfbfmHk6/Qz4JlSgHZIqgOS1FTsmXFvBUe4moHR/QFDEXnz2LroRIavt9gWZPE3z1FIcjwSmtUzvpE8gZud+dKVVrjm8YPseq+IJ/AwRRlkUR5D0vf1TwEm0yQ7E+EotFvibQ=" + }`) + + // Initial call + msg1 := &Message{} + err := json.Unmarshal(msgJSON, msg1) + require.NoError(t, err) + msg1.Status = 0 + + // Second call + msg2 := &Message{} + err = json.Unmarshal(msgJSON, msg2) + require.NoError(t, err) + msg2.Status = 1 + + // Final call + msg3 := &Message{} + err = json.Unmarshal(msgJSON, msg3) + require.NoError(t, err) + msg3.Status = 2 + + cipher, err := idbCrypto.NewCipher( + []byte("testPass"), []byte("testSalt"), 128, + csprng.NewSystemRNG()) + require.NoError(t, err) + testString := "test_duplicateUpsertMessage" + eventModel, err := newWASMModel(testString, cipher, dummyEU) + require.NoError(t, err) + + uuid, err := eventModel.upsertMessage(msg1) + require.NoError(t, err) + require.NotEqual(t, uuid, 0) + + uuid2, err := eventModel.upsertMessage(msg2) + require.NoError(t, err) + require.Equal(t, uuid, uuid2) + + uuid3, err := eventModel.upsertMessage(msg3) + require.NoError(t, err) + require.Equal(t, uuid, uuid3) + + msgID, err := message.UnmarshalID(msg1.MessageID) + require.NoError(t, err) + + modelMsg, err := eventModel.GetMessage(msgID) + require.NoError(t, err) + require.Equal(t, channels.Delivered, modelMsg.Status) +} diff --git a/indexedDb/impl/channels/init.go b/indexedDb/impl/channels/init.go index ecc16da447b0db684ca55031a26e6a3d19206320..7a55c6d09823124e26360558aa7d3555bdbdb462 100644 --- a/indexedDb/impl/channels/init.go +++ b/indexedDb/impl/channels/init.go @@ -16,43 +16,42 @@ import ( jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/channels" - cryptoChannel "gitlab.com/elixxir/crypto/channel" + idbCrypto "gitlab.com/elixxir/crypto/indexedDb" "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 + +// eventUpdate takes an event type and JSON object from +// bindings/channelsCallbacks.go. +type eventUpdate func(eventType int64, jsonMarshallable any) // 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) +func NewWASMEventModel(databaseName string, encryption idbCrypto.Cipher, + eventCallback eventUpdate) (channels.EventModel, error) { + return newWASMModel(databaseName, encryption, eventCallback) } // 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) { +func newWASMModel(databaseName string, encryption idbCrypto.Cipher, + eventCallback eventUpdate) (*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 +61,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 }) @@ -86,11 +77,9 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, } wrapper := &wasmModel{ - db: db, - cipher: encryption, - receivedMessageCB: messageReceivedCB, - deletedMessageCB: deletedMessageCB, - mutedUserCB: mutedUserCB, + db: db, + cipher: encryption, + eventCallback: eventCallback, } return wrapper, nil } @@ -150,15 +139,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 b84f13dc205ecd8bfe2466ea9ecb827770f31754..7213825175820be161e1b6e8d0d29efde49c545f 100644 --- a/indexedDb/impl/channels/main.go +++ b/indexedDb/impl/channels/main.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/wasm-utils/exception" "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -51,10 +52,27 @@ var channelsCmd = &cobra.Command{ jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER) jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.") - m := &manager{ - wtm: worker.NewThreadManager("ChannelsIndexedDbWorker", true), + tm, err := worker.NewThreadManager("ChannelsIndexedDbWorker", true) + if err != nil { + exception.ThrowTrace(err) } + m := &manager{wtm: tm} m.registerCallbacks() + + m.wtm.RegisterMessageChannelCallback(worker.LoggerTag, + func(port js.Value, channelName string) { + p := worker.DefaultParams() + p.MessageLogging = false + err = logging.EnableThreadLogging( + logLevel, threadLogLevel, 0, channelName, port) + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } + + jww.INFO.Print("TEST channel") + }) + m.wtm.SignalReady() // Indicate to the Javascript caller that the WASM is ready by resolving @@ -68,13 +86,20 @@ var channelsCmd = &cobra.Command{ } var ( - logLevel jww.Threshold + logLevel jww.Threshold + threadLogLevel jww.Threshold ) func init() { // Initialize all startup flags - channelsCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + channelsCmd.Flags().IntVarP((*int)(&logLevel), + "logLevel", "l", int(jww.LevelDebug), "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.") + channelsCmd.Flags().IntVarP((*int)(&threadLogLevel), + "threadLogLevel", "m", int(jww.LevelDebug), + "The log level when outputting to the worker file buffer. "+ + "0 = TRACE, 1 = DEBUG, 2 = INFO, 3 = WARN, 4 = ERROR, "+ + "5 = CRITICAL, 6 = FATAL, -1 = disabled.") } diff --git a/indexedDb/impl/channels/model.go b/indexedDb/impl/channels/model.go index e5d3e00aa5209985de60a77f5330a53208b1af88..702d489b04d4eb6bfac510a5b9e77f753f411cee 100644 --- a/indexedDb/impl/channels/model.go +++ b/indexedDb/impl/channels/model.go @@ -56,7 +56,7 @@ type Message struct { Status uint8 `json:"status"` Hidden bool `json:"hidden"` Pinned bool `json:"pinned"` // Index - Text []byte `json:"text"` + Text string `json:"text"` Type uint16 `json:"type"` Round uint64 `json:"round"` diff --git a/indexedDb/impl/dm/callbacks.go b/indexedDb/impl/dm/callbacks.go index 380f04d9953fdda65c4b0d80a66e7b40a7edc2b1..a081a83dca9adf61a78e48101239edfd6a22c0b1 100644 --- a/indexedDb/impl/dm/callbacks.go +++ b/indexedDb/impl/dm/callbacks.go @@ -10,15 +10,15 @@ package main import ( - "crypto/ed25519" "encoding/json" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/dm" - cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/fastRNG" + idbCrypto "gitlab.com/elixxir/crypto/indexedDb" + "gitlab.com/elixxir/wasm-utils/exception" wDm "gitlab.com/elixxir/xxdk-wasm/indexedDb/worker/dm" "gitlab.com/elixxir/xxdk-wasm/worker" "gitlab.com/xx_network/crypto/csprng" @@ -42,192 +42,226 @@ func (m *manager) registerCallbacks() { 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.DeleteMessageTag, m.deleteMessageCB) m.wtm.RegisterCallback(wDm.GetConversationTag, m.getConversationCB) m.wtm.RegisterCallback(wDm.GetConversationsTag, m.getConversationsCB) } // newWASMEventModelCB is the callback for NewWASMEventModel. Returns an empty // slice on success or an error message on failure. -func (m *manager) newWASMEventModelCB(data []byte) ([]byte, error) { +func (m *manager) newWASMEventModelCB(message []byte, reply func(message []byte)) { var msg wDm.NewWASMEventModelMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return []byte{}, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + reply([]byte(errors.Wrapf(err, + "failed to JSON unmarshal %T from main thread", msg).Error())) + return } // Create new encryption cipher rng := fastRNG.NewStreamGenerator(12, 1024, csprng.NewSystemRNG) - encryption, err := cryptoChannel.NewCipherFromJSON( + encryption, err := idbCrypto.NewCipherFromJSON( []byte(msg.EncryptionJSON), rng.GetStream()) if err != nil { - return []byte{}, errors.Errorf("failed to JSON unmarshal channel "+ - "cipher from main thread: %+v", err) + reply([]byte(errors.Wrap(err, + "failed to JSON unmarshal Cipher from main thread").Error())) + return } m.model, err = NewWASMEventModel( - msg.DatabaseName, encryption, m.messageReceivedCallback) + msg.DatabaseName, encryption, m.eventUpdateCallback) if err != nil { - return []byte(err.Error()), nil + reply([]byte(err.Error())) + return } - return []byte{}, nil + reply(nil) } -// messageReceivedCallback sends calls to the MessageReceivedCallback in the -// main thread. -// -// messageReceivedCallback adhere to the MessageReceivedCallback type. -func (m *manager) messageReceivedCallback(uuid uint64, pubKey ed25519.PublicKey, - messageUpdate, conversationUpdate bool) { +// eventUpdateCallback JSON marshals the interface and sends it to the main +// thread the with the event type to be sent on the EventUpdate callback. +func (m *manager) eventUpdateCallback(eventType int64, jsonMarshallable any) { + jsonData, err := json.Marshal(jsonMarshallable) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to JSON marshal %T for EventUpdate "+ + "callback: %+v", jsonMarshallable, err) + } + // Package parameters for sending - msg := &wDm.MessageReceivedCallbackMessage{ - UUID: uuid, - PubKey: pubKey, - MessageUpdate: messageUpdate, - ConversationUpdate: conversationUpdate, + msg := wDm.EventUpdateCallbackMessage{ + EventType: eventType, + JsonData: jsonData, } data, err := json.Marshal(msg) if err != nil { - jww.ERROR.Printf( - "Could not JSON marshal MessageReceivedCallbackMessage: %+v", err) - return + exception.Throwf("[DM] Could not JSON marshal %T for EventUpdate "+ + "callback: %+v", msg, err) } // Send it to the main thread - m.wtm.SendMessage(wDm.MessageReceivedCallbackTag, data) + err = m.wtm.SendNoResponse(wDm.EventUpdateCallbackTag, data) + if err != nil { + exception.Throwf( + "[DM] Could not send message for EventUpdate callback: %+v", err) + } } // receiveCB is the callback for wasmModel.Receive. Returns a UUID of 0 on error // or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveCB(data []byte) ([]byte, error) { +func (m *manager) receiveCB(message []byte, reply func(message []byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for Receive "+ + "from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.Receive( - msg.MessageID, msg.Nickname, msg.Text, msg.PartnerKey, msg.SenderKey, msg.DmToken, - msg.Codeset, msg.Timestamp, msg.Round, msg.MType, msg.Status) + msg.MessageID, msg.Nickname, msg.Text, msg.PartnerKey, msg.SenderKey, + msg.DmToken, msg.Codeset, msg.Timestamp, msg.Round, msg.MType, + msg.Status) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[DM] Could not JSON marshal UUID for Receive: %+v", err) } - return uuidData, nil + + reply(replyMsg) } // receiveTextCB is the callback for wasmModel.ReceiveText. Returns a UUID of 0 // on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveTextCB(data []byte) ([]byte, error) { +func (m *manager) receiveTextCB(message []byte, reply func(message []byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return []byte{}, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for "+ + "ReceiveText from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveText( - msg.MessageID, msg.Nickname, string(msg.Text), msg.PartnerKey, msg.SenderKey, msg.DmToken, - msg.Codeset, msg.Timestamp, msg.Round, msg.Status) + msg.MessageID, msg.Nickname, string(msg.Text), msg.PartnerKey, + msg.SenderKey, msg.DmToken, msg.Codeset, msg.Timestamp, msg.Round, + msg.Status) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return []byte{}, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[DM] Could not JSON marshal UUID for ReceiveText: %+v", err) } - return uuidData, nil + reply(replyMsg) } // receiveReplyCB is the callback for wasmModel.ReceiveReply. Returns a UUID of // 0 on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveReplyCB(data []byte) ([]byte, error) { +func (m *manager) receiveReplyCB(message []byte, reply func(message []byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for "+ + "ReceiveReply from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveReply(msg.MessageID, msg.ReactionTo, msg.Nickname, string(msg.Text), msg.PartnerKey, msg.SenderKey, msg.DmToken, msg.Codeset, msg.Timestamp, msg.Round, msg.Status) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[DM] Could not JSON marshal UUID for ReceiveReply: %+v", err) } - return uuidData, nil + reply(replyMsg) } // receiveReactionCB is the callback for wasmModel.ReceiveReaction. Returns a // UUID of 0 on error or the JSON marshalled UUID (uint64) on success. -func (m *manager) receiveReactionCB(data []byte) ([]byte, error) { +func (m *manager) receiveReactionCB(message []byte, reply func(message []byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return zeroUUID, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal payload for "+ + "ReceiveReaction from main thread: %+v", err) + reply(zeroUUID) + return } uuid := m.model.ReceiveReaction(msg.MessageID, msg.ReactionTo, msg.Nickname, string(msg.Text), msg.PartnerKey, msg.SenderKey, msg.DmToken, msg.Codeset, msg.Timestamp, msg.Round, msg.Status) - uuidData, err := json.Marshal(uuid) + replyMsg, err := json.Marshal(uuid) if err != nil { - return zeroUUID, errors.Errorf("failed to JSON marshal UUID: %+v", err) + exception.Throwf( + "[DM] Could not JSON marshal UUID for ReceiveReaction: %+v", err) } - return uuidData, nil + reply(replyMsg) } // updateSentStatusCB is the callback for wasmModel.UpdateSentStatus. Always // returns nil; meaning, no response is supplied (or expected). -func (m *manager) updateSentStatusCB(data []byte) ([]byte, error) { +func (m *manager) updateSentStatusCB(message []byte, _ func([]byte)) { var msg wDm.TransferMessage - err := json.Unmarshal(data, &msg) + err := json.Unmarshal(message, &msg) if err != nil { - return nil, errors.Errorf( - "failed to JSON unmarshal %T from main thread: %+v", msg, err) + jww.ERROR.Printf("[DM] Could not JSON unmarshal %T for "+ + "UpdateSentStatus from main thread: %+v", msg, err) + return } m.model.UpdateSentStatus( msg.UUID, msg.MessageID, msg.Timestamp, msg.Round, msg.Status) - - return nil, nil } -// blockSenderCB is the callback for wasmModel.BlockSender. Always -// returns nil; meaning, no response is supplied (or expected). -func (m *manager) blockSenderCB(data []byte) ([]byte, error) { - m.model.BlockSender(data) - return nil, nil -} +// deleteMessageCB is the callback for wasmModel.DeleteMessage. Returns a JSON +// marshalled bool. +func (m *manager) deleteMessageCB(message []byte, reply func(message []byte)) { + var msg wDm.TransferMessage + err := json.Unmarshal(message, &msg) + if err != nil { + jww.ERROR.Printf("[DM] Could not JSON unmarshal %T for "+ + "UpdateSentStatus from main thread: %+v", msg, err) + reply([]byte{0}) + return + } -// unblockSenderCB is the callback for wasmModel.UnblockSender. Always -// returns nil; meaning, no response is supplied (or expected). -func (m *manager) unblockSenderCB(data []byte) ([]byte, error) { - m.model.UnblockSender(data) - return nil, nil + if m.model.DeleteMessage(msg.MessageID, msg.SenderKey) { + reply([]byte{1}) + return + } + reply([]byte{0}) } // getConversationCB is the callback for wasmModel.GetConversation. // Returns nil on error or the JSON marshalled Conversation on success. -func (m *manager) getConversationCB(data []byte) ([]byte, error) { - result := m.model.GetConversation(data) - return json.Marshal(result) +func (m *manager) getConversationCB(message []byte, reply func(message []byte)) { + result := m.model.GetConversation(message) + replyMessage, err := json.Marshal(result) + if err != nil { + exception.Throwf("[DM] Could not JSON marshal %T for "+ + "GetConversation: %+v", result, err) + } + reply(replyMessage) } // getConversationsCB is the callback for wasmModel.GetConversations. // Returns nil on error or the JSON marshalled list of Conversation on success. -func (m *manager) getConversationsCB(_ []byte) ([]byte, error) { +func (m *manager) getConversationsCB(_ []byte, reply func(message []byte)) { result := m.model.GetConversations() - return json.Marshal(result) + replyMessage, err := json.Marshal(result) + if err != nil { + exception.Throwf("[DM] Could not JSON marshal %T for "+ + "GetConversations: %+v", result, err) + } + reply(replyMessage) } diff --git a/indexedDb/impl/dm/dmIndexedDbWorker.js b/indexedDb/impl/dm/dmIndexedDbWorker.js index 8a5fdbf8ad9a02967b408985a0219647003eaf7e..41a5492fb799fe8c27db59252c18d6d8ad454c90 100644 --- a/indexedDb/impl/dm/dmIndexedDbWorker.js +++ b/indexedDb/impl/dm/dmIndexedDbWorker.js @@ -12,6 +12,10 @@ const isReady = new Promise((resolve) => { }); const go = new Go(); +go.argv = [ + '--logLevel=2', + '--threadLogLevel=2', +] const binPath = 'xxdk-dmIndexedDkWorker.wasm' WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then(async (result) => { go.run(result.instance); diff --git a/indexedDb/impl/dm/implementation.go b/indexedDb/impl/dm/implementation.go index 1c8e18050d8c36c32ede354f9afaa9a6af1993db..e34bedb879c69daafbf0f5df1eb89c788b343b46 100644 --- a/indexedDb/impl/dm/implementation.go +++ b/indexedDb/impl/dm/implementation.go @@ -21,37 +21,39 @@ import ( "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/client/v4/cmix/rounds" "gitlab.com/elixxir/client/v4/dm" - cryptoChannel "gitlab.com/elixxir/crypto/channel" + idbCrypto "gitlab.com/elixxir/crypto/indexedDb" "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" + "gitlab.com/xx_network/primitives/netTime" ) // wasmModel implements dm.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 MessageReceivedCallback + db *idb.Database + cipher idbCrypto.Cipher + eventCallback eventUpdate } // 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 @@ -80,7 +82,7 @@ func (w *wasmModel) upsertConversation(nickname string, // NOTE: ID is not set inside this function because we want to use the // autoincrement key by default. If you are trying to overwrite an existing // message, then you need to set it manually yourself. -func buildMessage(messageID, parentID, text []byte, partnerKey, +func buildMessage(messageID, parentID []byte, text string, partnerKey []byte, senderKey ed25519.PublicKey, timestamp time.Time, round id.Round, mType dm.MessageType, codeset uint8, status dm.Status) *Message { return &Message{ @@ -178,13 +180,7 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID, } // Extract the existing Message and update the Status - newMessage := &Message{} - err = json.Unmarshal([]byte(utils.JsToJson(currentMsg)), newMessage) - if err != nil { - jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr, - "Could not JSON unmarshal message: %+v", err)) - return - } + newMessage, err := valueToMessage(currentMsg) newMessage.Status = uint8(status) if !messageID.Equals(message.ID{}) { @@ -208,8 +204,12 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID, jww.TRACE.Printf("[DM indexedDB] Calling ReceiveMessageCB(%v, %v, t, f)", uuid, newMessage.ConversationPubKey) - go w.receivedMessageCB(uuid, newMessage.ConversationPubKey, - true, false) + go w.eventCallback(bindings.DmMessageReceived, bindings.DmMessageReceivedJSON{ + UUID: uuid, + PubKey: newMessage.ConversationPubKey, + MessageUpdate: true, + ConversationUpdate: false, + }) } // receiveWrapper is a higher-level wrapper of upsertMessage. @@ -231,11 +231,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,16 +268,15 @@ 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 } } // Handle encryption, if it is present - textBytes := []byte(data) if w.cipher != nil { - textBytes, err = w.cipher.Encrypt(textBytes) + data, err = w.cipher.Encrypt([]byte(data)) if err != nil { return 0, err } @@ -288,7 +287,7 @@ func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, n parentIdBytes = parentID.Marshal() } - msgToInsert := buildMessage(messageID.Bytes(), parentIdBytes, textBytes, + msgToInsert := buildMessage(messageID.Bytes(), parentIdBytes, data, partnerKey, senderKey, timestamp, round.ID, mType, codeset, status) uuid, err := w.upsertMessage(msgToInsert) @@ -298,7 +297,12 @@ func (w *wasmModel) receiveWrapper(messageID message.ID, parentID *message.ID, n jww.TRACE.Printf("[DM indexedDB] Calling ReceiveMessageCB(%v, %v, f, %t)", uuid, partnerKey, conversationUpdated) - go w.receivedMessageCB(uuid, partnerKey, false, conversationUpdated) + go w.eventCallback(bindings.DmMessageReceived, bindings.DmMessageReceivedJSON{ + UUID: uuid, + PubKey: partnerKey, + MessageUpdate: false, + ConversationUpdate: conversationUpdated, + }) return uuid, nil } @@ -349,14 +353,63 @@ 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) +} + +// DeleteMessage deletes the message with the given message.ID belonging to +// the sender. If the message exists and belongs to the sender, then it is +// deleted and DeleteMessage returns true. If it does not exist, it returns +// false. +func (w *wasmModel) DeleteMessage(messageID message.ID, senderPubKey ed25519.PublicKey) bool { + parentErr := "failed to DeleteMessage" + msgId := impl.EncodeBytes(messageID.Marshal()) + + // Use the key to get the existing Message + currentMsg, err := impl.GetIndex(w.db, messageStoreName, + messageStoreMessageIndex, msgId) + if err != nil { + jww.ERROR.Printf("%s: %+v", parentErr, err) + return false + } + + // Convert the js.Value to a proper object + msgObj, err := valueToMessage(currentMsg) + if err != nil { + jww.ERROR.Printf("%s: %+v", parentErr, err) + return false + } + + // Ensure the public keys match + if !bytes.Equal(msgObj.SenderPubKey, senderPubKey) { + jww.ERROR.Printf("%s: %s", parentErr, "Public keys do not match") + return false + } + + // Perform the delete + err = impl.DeleteIndex(w.db, messageStoreName, messageStoreMessageIndex, + msgPkeyName, msgId) + if err != nil { + jww.ERROR.Printf("%s: %+v", parentErr, err) + return false + } + + go w.eventCallback(bindings.DmMessageReceived, bindings.DmMessageDeletedJSON{ + MessageID: messageID, + }) + return true } // GetConversation returns the conversation held by the model (receiver). @@ -369,11 +422,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, } } @@ -411,12 +464,18 @@ 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 } + +// valueToMessage is a helper for converting js.Value to Message. +func valueToMessage(msgObj js.Value) (*Message, error) { + resultMsg := &Message{} + return resultMsg, json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg) +} diff --git a/indexedDb/impl/dm/implementation_test.go b/indexedDb/impl/dm/implementation_test.go index 8e05e6af5fff85af79ea0f15be9dc1e2f5a77c12..3ccadb2d8202f2b435a5331a52c7a569cd995ffa 100644 --- a/indexedDb/impl/dm/implementation_test.go +++ b/indexedDb/impl/dm/implementation_test.go @@ -14,11 +14,12 @@ import ( "crypto/ed25519" "encoding/json" "fmt" + "github.com/stretchr/testify/require" "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" @@ -28,7 +29,7 @@ import ( jww "github.com/spf13/jwalterweatherman" ) -func dummyReceivedMessageCB(uint64, ed25519.PublicKey, bool, bool) {} +var dummyEU = func(int64, any) {} func TestMain(m *testing.M) { jww.SetStdoutThreshold(jww.LevelDebug) @@ -37,8 +38,7 @@ func TestMain(m *testing.M) { // Test simple receive of a new message for a new conversation. func TestImpl_Receive(t *testing.T) { - m, err := newWASMModel("TestImpl_Receive", nil, - dummyReceivedMessageCB) + m, err := newWASMModel("TestImpl_Receive", nil, dummyEU) if err != nil { t.Fatal(err.Error()) } @@ -90,8 +90,7 @@ func TestImpl_Receive(t *testing.T) { // Test happy path. Insert some conversations and check they exist. func TestImpl_GetConversations(t *testing.T) { - m, err := newWASMModel("TestImpl_GetConversations", nil, - dummyReceivedMessageCB) + m, err := newWASMModel("TestImpl_GetConversations", nil, dummyEU) if err != nil { t.Fatal(err.Error()) } @@ -102,7 +101,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()) } @@ -126,35 +125,68 @@ func TestImpl_GetConversations(t *testing.T) { // Test happy path toggling between blocked/unblocked in a Conversation. func TestWasmModel_BlockSender(t *testing.T) { - m, err := newWASMModel("TestWasmModel_BlockSender", nil, dummyReceivedMessageCB) + m, err := newWASMModel("TestWasmModel_BlockSender", nil, dummyEU) if err != nil { t.Fatal(err.Error()) } // 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") } } + +// Test failed and successful deletes +func TestWasmModel_DeleteMessage(t *testing.T) { + m, err := newWASMModel("TestWasmModel_DeleteMessage", nil, dummyEU) + if err != nil { + t.Fatal(err.Error()) + } + + // Insert test message + testBytes := []byte("test") + testBadBytes := []byte("uwu") + testMsgId := message.DeriveChannelMessageID(&id.ID{1}, 0, testBytes) + testMsg := &Message{ + MessageID: testMsgId.Marshal(), + ConversationPubKey: testBytes, + ParentMessageID: nil, + Timestamp: time.Now(), + SenderPubKey: testBytes, + CodesetVersion: 5, + Status: 5, + Text: "", + Type: 5, + Round: 5, + } + _, err = m.upsertMessage(testMsg) + require.NoError(t, err) + + // Non-matching pub key, should fail to delete + require.False(t, m.DeleteMessage(testMsgId, testBadBytes)) + + // Correct pub key, should have deleted + require.True(t, m.DeleteMessage(testMsgId, testBytes)) +} diff --git a/indexedDb/impl/dm/init.go b/indexedDb/impl/dm/init.go index f99f4ef3e2d7f46f9a5672c8ea998c4afdbe1a77..0c33f614fc7cf2b9e2bbc041059e2e8b3bf0b90e 100644 --- a/indexedDb/impl/dm/init.go +++ b/indexedDb/impl/dm/init.go @@ -10,14 +10,13 @@ package main import ( - "crypto/ed25519" "syscall/js" "github.com/hack-pad/go-indexeddb/idb" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/dm" - cryptoChannel "gitlab.com/elixxir/crypto/channel" + idbCrypto "gitlab.com/elixxir/crypto/indexedDb" "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" ) @@ -25,36 +24,33 @@ import ( // migration purposes. const currentVersion uint = 1 -// MessageReceivedCallback is called any time a message is received or updated. -// -// messageUpdate is true if the Message already exists and was edited. -// conversationUpdate is true if the Conversation was created or modified. -type MessageReceivedCallback func( - uuid uint64, pubKey ed25519.PublicKey, messageUpdate, conversationUpdate bool) +// eventUpdate takes an event type and JSON object from bindings/dm.go. +type eventUpdate func(eventType int64, jsonMarshallable any) // 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, - cb MessageReceivedCallback) (dm.EventModel, error) { - return newWASMModel(databaseName, encryption, cb) +func NewWASMEventModel(databaseName string, encryption idbCrypto.Cipher, + eventCallback eventUpdate) (dm.EventModel, error) { + return newWASMModel(databaseName, encryption, eventCallback) } // newWASMModel creates the given [idb.Database] and returns a wasmModel. -func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, - cb MessageReceivedCallback) (*wasmModel, error) { +func newWASMModel(databaseName string, encryption idbCrypto.Cipher, + eventCallback eventUpdate) (*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) @@ -79,7 +75,11 @@ func newWASMModel(databaseName string, encryption cryptoChannel.Cipher, return nil, ctx.Err() } - wrapper := &wasmModel{db: db, receivedMessageCB: cb, cipher: encryption} + wrapper := &wasmModel{ + db: db, + cipher: encryption, + eventCallback: eventCallback, + } return wrapper, nil } diff --git a/indexedDb/impl/dm/main.go b/indexedDb/impl/dm/main.go index 96fae8e6fbdbce3767739891d4d7ea466e08149c..0034e9265d86a79c76a2b604ae725e846688f09f 100644 --- a/indexedDb/impl/dm/main.go +++ b/indexedDb/impl/dm/main.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/wasm-utils/exception" "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -52,10 +53,27 @@ var dmCmd = &cobra.Command{ jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER) jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.") - m := &manager{ - wtm: worker.NewThreadManager("DmIndexedDbWorker", true), + tm, err := worker.NewThreadManager("DmIndexedDbWorker", true) + if err != nil { + exception.ThrowTrace(err) } + m := &manager{wtm: tm} m.registerCallbacks() + + m.wtm.RegisterMessageChannelCallback(worker.LoggerTag, + func(port js.Value, channelName string) { + p := worker.DefaultParams() + p.MessageLogging = false + err = logging.EnableThreadLogging( + logLevel, threadLogLevel, 0, channelName, port) + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } + + jww.INFO.Print("TEST channel") + }) + m.wtm.SignalReady() // Indicate to the Javascript caller that the WASM is ready by resolving @@ -69,13 +87,20 @@ var dmCmd = &cobra.Command{ } var ( - logLevel jww.Threshold + logLevel jww.Threshold + threadLogLevel jww.Threshold ) func init() { // Initialize all startup flags - dmCmd.Flags().IntVarP((*int)(&logLevel), "logLevel", "l", 2, + dmCmd.Flags().IntVarP((*int)(&logLevel), + "logLevel", "l", int(jww.LevelDebug), "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.") + dmCmd.Flags().IntVarP((*int)(&threadLogLevel), + "threadLogLevel", "m", int(jww.LevelDebug), + "The log level when outputting to the worker file buffer. "+ + "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..6893fc3a6a7fffd95eb31e389e8352040e797b9f 100644 --- a/indexedDb/impl/dm/model.go +++ b/indexedDb/impl/dm/model.go @@ -46,7 +46,7 @@ type Message struct { SenderPubKey []byte `json:"sender_pub_key"` // Index CodesetVersion uint8 `json:"codeset_version"` Status uint8 `json:"status"` - Text []byte `json:"text"` + Text string `json:"text"` Type uint16 `json:"type"` Round uint64 `json:"round"` } @@ -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..5bbfcc7d89a3dd8675c47824725c3add02b38f8e --- /dev/null +++ b/indexedDb/impl/state/callbacks.go @@ -0,0 +1,94 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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/wasm-utils/exception" + "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" + 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 impl.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(message []byte, reply func(message []byte)) { + var msg stateWorker.NewStateMessage + err := json.Unmarshal(message, &msg) + if err != nil { + reply([]byte(errors.Wrapf(err, + "failed to JSON unmarshal %T from main thread", msg).Error())) + return + } + + m.model, err = NewState(msg.DatabaseName) + if err != nil { + reply([]byte(err.Error())) + return + } + + reply(nil) +} + +// setCB is the callback for stateModel.Set. +// Returns nil on error or the resulting byte data on success. +func (m *manager) setCB(message []byte, reply func(message []byte)) { + var msg stateWorker.TransferMessage + err := json.Unmarshal(message, &msg) + if err != nil { + reply([]byte(errors.Wrapf(err, + "failed to JSON unmarshal %T from main thread", msg).Error())) + return + } + + err = m.model.Set(msg.Key, msg.Value) + if err != nil { + reply([]byte(err.Error())) + return + } + + reply(nil) +} + +// getCB is the callback for stateModel.Get. +// Returns nil on error or the resulting byte data on success. +func (m *manager) getCB(message []byte, reply func(message []byte)) { + key := string(message) + result, err := m.model.Get(key) + msg := stateWorker.TransferMessage{ + Key: key, + Value: result, + Error: err.Error(), + } + + replyMessage, err := json.Marshal(msg) + if err != nil { + exception.Throwf("Could not JSON marshal %T for Get: %+v", msg, err) + } + + reply(replyMessage) +} 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..14aab3a896ea8dc248f8d7f8b9f7c2bf6b42b87d --- /dev/null +++ b/indexedDb/impl/state/init.go @@ -0,0 +1,83 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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/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) (impl.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..2344486a2c3b1c6ab55db13110d4d4305b16ed0f --- /dev/null +++ b/indexedDb/impl/state/main.go @@ -0,0 +1,105 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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/wasm-utils/exception" + "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. + stateCmd.SetArgs(os.Args) + + err := stateCmd.Execute() + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +var stateCmd = &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.") + tm, err := worker.NewThreadManager("DmIndexedDbWorker", true) + if err != nil { + exception.ThrowTrace(err) + } + m := &manager{wtm: tm} + m.registerCallbacks() + + m.wtm.RegisterMessageChannelCallback(worker.LoggerTag, + func(port js.Value, channelName string) { + p := worker.DefaultParams() + p.MessageLogging = false + err = logging.EnableThreadLogging( + logLevel, threadLogLevel, 0, channelName, port) + if err != nil { + fmt.Printf("Failed to intialize logging: %+v", err) + os.Exit(1) + } + + jww.INFO.Print("TEST channel") + }) + + 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 + threadLogLevel jww.Threshold +) + +func init() { + // Initialize all startup flags + stateCmd.Flags().IntVarP((*int)(&logLevel), + "logLevel", "l", int(jww.LevelDebug), + "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.") + stateCmd.Flags().IntVarP((*int)(&threadLogLevel), + "threadLogLevel", "m", int(jww.LevelDebug), + "The log level when outputting to the worker file buffer. "+ + "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..cff3e34af865755070e6ff634465b1b82b12ced3 --- /dev/null +++ b/indexedDb/impl/state/stateIndexedDbWorker.js @@ -0,0 +1,25 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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(); +go.argv = [ + '--logLevel=2', + '--threadLogLevel=2', +] +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 b657240b7e7018a031365ffc8514c3fe54141471..d9c2fa8ed1eeb32ee7a71a879e5ae2e405f01e69 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" ) @@ -32,6 +32,13 @@ const ( ErrDoesNotExist = "result is undefined" ) +// WebState defines an interface for setting persistent state in a KV format +// specifically for web-based implementations. +type WebState interface { + Get(key string) ([]byte, error) + Set(key string, value []byte) error +} + // NewContext builds a context for indexedDb operations. func NewContext() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), dbTimeout) diff --git a/indexedDb/worker/channels/implementation.go b/indexedDb/worker/channels/implementation.go index 9639bc185095e74cbf4b3e63256fead39bbcef82..d6495de4fae213db99481992e38306a49f2bcd4d 100644 --- a/indexedDb/worker/channels/implementation.go +++ b/indexedDb/worker/channels/implementation.go @@ -41,12 +41,17 @@ func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) { return } - w.wm.SendMessage(JoinChannelTag, data, nil) + if err = w.wm.SendNoResponse(JoinChannelTag, data); err != nil { + jww.FATAL.Panicf("[CH] Failed to send to %q: %+v", JoinChannelTag, err) + } } // LeaveChannel is called whenever a channel is left locally. func (w *wasmModel) LeaveChannel(channelID *id.ID) { - w.wm.SendMessage(LeaveChannelTag, channelID.Marshal(), nil) + err := w.wm.SendNoResponse(LeaveChannelTag, channelID.Marshal()) + if err != nil { + jww.FATAL.Panicf("[CH] Failed to send to %q: %+v", LeaveChannelTag, err) + } } // ReceiveMessage is called whenever a message is received on a given channel. @@ -81,27 +86,20 @@ func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID, return 0 } - uuidChan := make(chan uint64) - w.wm.SendMessage(ReceiveMessageTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+ - "ReceiveMessage: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) + response, err := w.wm.SendMessage(ReceiveMessageTag, data) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", ReceiveMessageTag, err) + } - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+ - "the worker about ReceiveMessage", worker.ResponseTimeout) + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[CH] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveMessageTag, err) + return 0 } - return 0 + return uuid } // ReceiveReplyMessage is JSON marshalled and sent to the worker for @@ -149,27 +147,20 @@ func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID, return 0 } - uuidChan := make(chan uint64) - w.wm.SendMessage(ReceiveReplyTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+ - "ReceiveReply: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) + response, err := w.wm.SendMessage(ReceiveReplyTag, data) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", ReceiveReplyTag, err) + } - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+ - "the worker about ReceiveReply", worker.ResponseTimeout) + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[CH] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveReplyTag, err) + return 0 } - return 0 + return uuid } // ReceiveReaction is called whenever a reaction to a message is received on a @@ -211,27 +202,20 @@ func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID, return 0 } - uuidChan := make(chan uint64) - w.wm.SendMessage(ReceiveReactionTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+ - "ReceiveReaction: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) + response, err := w.wm.SendMessage(ReceiveReactionTag, data) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", ReceiveReactionTag, err) + } - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("[CH] Timed out after %s waiting for response from "+ - "the worker about ReceiveReply", worker.ResponseTimeout) + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[CH] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveReactionTag, err) + return 0 } - return 0 + return uuid } // MessageUpdateInfo is JSON marshalled and sent to the worker for @@ -301,29 +285,22 @@ func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID, "could not JSON marshal payload for UpdateFromUUID: %+v", err) } - errChan := make(chan error) - w.wm.SendMessage(UpdateFromUUIDTag, data, func(data []byte) { - if data != nil { - errChan <- errors.New(string(data)) - } else { - errChan <- nil - } - }) - - select { - case err = <-errChan: - return err - case <-time.After(worker.ResponseTimeout): - return errors.Errorf("timed out after %s waiting for response from "+ - "the worker about UpdateFromUUID", worker.ResponseTimeout) + response, err := w.wm.SendMessage(UpdateFromUUIDTag, data) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", UpdateFromUUIDTag, err) + } else if len(response) > 0 { + return errors.New(string(response)) } + + return nil } // UuidError is JSON marshalled and sent to the worker for // [wasmModel.UpdateFromMessageID]. type UuidError struct { UUID uint64 `json:"uuid"` - Error []byte `json:"error"` + Error string `json:"error"` } // UpdateFromMessageID is called whenever a message with the message ID is @@ -367,29 +344,20 @@ func (w *wasmModel) UpdateFromMessageID(messageID message.ID, "UpdateFromMessageID: %+v", err) } - uuidChan := make(chan uint64) - errChan := make(chan error) - w.wm.SendMessage(UpdateFromMessageIDTag, data, - func(data []byte) { - var ue UuidError - if err = json.Unmarshal(data, &ue); err != nil { - errChan <- errors.Errorf("could not JSON unmarshal response "+ - "to UpdateFromMessageID: %+v", err) - } else if ue.Error != nil { - errChan <- errors.New(string(ue.Error)) - } else { - uuidChan <- ue.UUID - } - }) - - select { - case uuid := <-uuidChan: - return uuid, nil - case err = <-errChan: - return 0, err - case <-time.After(worker.ResponseTimeout): - return 0, errors.Errorf("timed out after %s waiting for response from "+ - "the worker about UpdateFromMessageID", worker.ResponseTimeout) + response, err := w.wm.SendMessage(UpdateFromMessageIDTag, data) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", UpdateFromMessageIDTag, err) + } + + var ue UuidError + if err = json.Unmarshal(response, &ue); err != nil { + return 0, errors.Errorf("could not JSON unmarshal response to %q: %+v", + UpdateFromMessageIDTag, err) + } else if len(ue.Error) > 0 { + return 0, errors.New(ue.Error) + } else { + return ue.UUID, nil } } @@ -403,50 +371,37 @@ type GetMessageMessage struct { // GetMessage returns the message with the given [channel.MessageID]. func (w *wasmModel) GetMessage( messageID message.ID) (channels.ModelMessage, error) { - msgChan := make(chan GetMessageMessage) - w.wm.SendMessage(GetMessageTag, messageID.Marshal(), - func(data []byte) { - var msg GetMessageMessage - err := json.Unmarshal(data, &msg) - if err != nil { - jww.ERROR.Printf("[CH] Could not JSON unmarshal response to "+ - "GetMessage: %+v", err) - } - msgChan <- msg - }) - - select { - case msg := <-msgChan: - if msg.Error != "" { - return channels.ModelMessage{}, errors.New(msg.Error) - } - return msg.Message, nil - case <-time.After(worker.ResponseTimeout): - return channels.ModelMessage{}, errors.Errorf("timed out after %s "+ - "waiting for response from the worker about GetMessage", - worker.ResponseTimeout) + + response, err := w.wm.SendMessage(GetMessageTag, messageID.Marshal()) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", GetMessageTag, err) + } + + var msg GetMessageMessage + if err = json.Unmarshal(response, &msg); err != nil { + return channels.ModelMessage{}, errors.Wrapf(err, + "[CH] Could not JSON unmarshal response to %q", GetMessageTag) } + + if msg.Error != "" { + return channels.ModelMessage{}, errors.New(msg.Error) + } + + return msg.Message, nil } // DeleteMessage removes a message with the given messageID from storage. func (w *wasmModel) DeleteMessage(messageID message.ID) error { - errChan := make(chan error) - w.wm.SendMessage(DeleteMessageTag, messageID.Marshal(), - func(data []byte) { - if data != nil { - errChan <- errors.New(string(data)) - } else { - errChan <- nil - } - }) - - select { - case err := <-errChan: - return err - case <-time.After(worker.ResponseTimeout): - return errors.Errorf("timed out after %s waiting for response from "+ - "the worker about DeleteMessage", worker.ResponseTimeout) + response, err := w.wm.SendMessage(DeleteMessageTag, messageID.Marshal()) + if err != nil { + jww.FATAL.Panicf( + "[CH] Failed to send to %q: %+v", DeleteMessageTag, err) + } else if len(response) > 0 { + return errors.New(string(response)) } + + return nil } // MuteUserMessage is JSON marshalled and sent to the worker for @@ -472,5 +427,8 @@ func (w *wasmModel) MuteUser( return } - w.wm.SendMessage(MuteUserTag, data, nil) + err = w.wm.SendNoResponse(MuteUserTag, data) + if err != nil { + jww.FATAL.Panicf("[CH] Failed to send to %q: %+v", MuteUserTag, err) + } } diff --git a/indexedDb/worker/channels/init.go b/indexedDb/worker/channels/init.go index 2ee630caf43177460a19f14d2b953d3a03b9c687..7dbc56e6b77499e6c92dfb9903a72e06af77383a 100644 --- a/indexedDb/worker/channels/init.go +++ b/indexedDb/worker/channels/init.go @@ -10,47 +10,30 @@ package channels import ( - "crypto/ed25519" "encoding/json" + "github.com/pkg/errors" - "time" 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" + idbCrypto "gitlab.com/elixxir/crypto/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/logging" "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) - // 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 { +func NewWASMEventModelBuilder(wasmJsPath string, encryption idbCrypto.Cipher, + channelCbs bindings.ChannelUICallbacks) channels.EventModelBuilder { fn := func(path string) (channels.EventModel, error) { return NewWASMEventModel(path, wasmJsPath, encryption, - messageReceivedCB, deletedMessageCB, mutedUserCB) + channelCbs) } return fn } @@ -64,10 +47,8 @@ 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) ( - channels.EventModel, error) { +func NewWASMEventModel(path, wasmJsPath string, encryption idbCrypto.Cipher, + cbs bindings.ChannelUICallbacks) (channels.EventModel, error) { databaseName := path + databaseSuffix wm, err := worker.NewManager(wasmJsPath, "channelsIndexedDb", true) @@ -75,17 +56,17 @@ 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 EventUpdate + wm.RegisterCallback(EventUpdateCallbackTag, eventUpdateCallbackHandler(cbs)) - // Register handler to manage messages for the MutedUserCallback - wm.RegisterCallback(MutedUserCallbackTag, - mutedUserCallbackHandler(mutedUserCB)) + // Create MessageChannel between worker and logger so that the worker logs + // are saved + err = worker.CreateMessageChannel(logging.GetLogger().Worker(), wm, + "channelsIndexedDbLogger", worker.LoggerTag) + if err != nil { + return nil, errors.Wrap(err, "Failed to create message channel "+ + "between channel indexedDb worker and logger") + } // Store the database name err = storage.StoreIndexedDb(databaseName) @@ -115,74 +96,37 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, return nil, err } - dataChan := make(chan []byte) - wm.SendMessage(NewWASMEventModelTag, 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) + response, err := wm.SendMessage(NewWASMEventModelTag, payload) + if err != nil { + return nil, errors.Wrapf(err, + "failed to send message %q", NewWASMEventModelTag) + } else if len(response) > 0 { + return nil, errors.New(string(response)) } 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"` -} - -// 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) - } +// EventUpdateCallbackMessage is JSON marshalled and received from the worker +// for the EventUpdate callback. +type EventUpdateCallbackMessage struct { + EventType int64 `json:"eventType"` + JsonData []byte `json:"jsonData"` } -// mutedUserCallbackHandler returns a handler to manage messages for the -// MutedUserCallback. -func mutedUserCallbackHandler(cb MutedUserCallback) func(data []byte) { - return func(data []byte) { - var msg MuteUserMessage - err := json.Unmarshal(data, &msg) - if err != nil { +// eventUpdateCallbackHandler returns a handler to manage messages for the +// [bindings.ChannelUICallbacks.EventUpdate] callback. +func eventUpdateCallbackHandler( + cbs bindings.ChannelUICallbacks) worker.ReceiverCallback { + return func(message []byte, _ func([]byte)) { + var msg EventUpdateCallbackMessage + if err := json.Unmarshal(message, &msg); err != nil { jww.ERROR.Printf( "Failed to JSON unmarshal %T from worker: %+v", msg, err) return } - cb(msg.ChannelID, msg.PubKey, msg.Unmute) + cbs.EventUpdate(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/dm/implementation.go b/indexedDb/worker/dm/implementation.go index 657f8a488705b38e55e9d75b825330c07b583ce0..3104eb48ae12a78afb0ae654230f1c2ac85584a1 100644 --- a/indexedDb/worker/dm/implementation.go +++ b/indexedDb/worker/dm/implementation.go @@ -29,19 +29,19 @@ type wasmModel struct { // TransferMessage is JSON marshalled and sent to the worker. type TransferMessage struct { - UUID uint64 `json:"uuid"` - MessageID message.ID `json:"messageID"` - ReactionTo message.ID `json:"reactionTo"` - Nickname string `json:"nickname"` - Text []byte `json:"text"` - PartnerKey ed25519.PublicKey `json:"partnerKey"` - SenderKey ed25519.PublicKey `json:"senderKey"` - DmToken uint32 `json:"dmToken"` - Codeset uint8 `json:"codeset"` + UUID uint64 `json:"uuid,omitempty"` + MessageID message.ID `json:"messageID,omitempty"` + ReactionTo message.ID `json:"reactionTo,omitempty"` + Nickname string `json:"nickname,omitempty"` + Text []byte `json:"text,omitempty"` + PartnerKey ed25519.PublicKey `json:"partnerKey,omitempty"` + SenderKey ed25519.PublicKey `json:"senderKey,omitempty"` + DmToken uint32 `json:"dmToken,omitempty"` + Codeset uint8 `json:"codeset,omitempty"` Timestamp time.Time `json:"timestamp"` Round rounds.Round `json:"round"` - MType dm.MessageType `json:"mType"` - Status dm.Status `json:"status"` + MType dm.MessageType `json:"mType,omitempty"` + Status dm.Status `json:"status,omitempty"` } func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte, @@ -64,31 +64,23 @@ func (w *wasmModel) Receive(messageID message.ID, nickname string, text []byte, data, err := json.Marshal(msg) if err != nil { jww.ERROR.Printf( - "Could not JSON marshal payload for TransferMessage: %+v", err) + "[DM] Could not JSON marshal payload for Receive: %+v", err) return 0 } - uuidChan := make(chan uint64) - w.wh.SendMessage(ReceiveTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to Receive: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about Receive", worker.ResponseTimeout) + response, err := w.wh.SendMessage(ReceiveTag, data) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveTag, err) + } + + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveTag, err) + return 0 } - return 0 + return uuid } func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string, @@ -114,27 +106,19 @@ func (w *wasmModel) ReceiveText(messageID message.ID, nickname, text string, return 0 } - uuidChan := make(chan uint64) - w.wh.SendMessage(ReceiveTextTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveText: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveText", worker.ResponseTimeout) + response, err := w.wh.SendMessage(ReceiveTextTag, data) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveTextTag, err) } - return 0 + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveTextTag, err) + return 0 + } + + return uuid } func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname, @@ -161,27 +145,19 @@ func (w *wasmModel) ReceiveReply(messageID, reactionTo message.ID, nickname, return 0 } - uuidChan := make(chan uint64) - w.wh.SendMessage(ReceiveReplyTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveReply: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveReply", worker.ResponseTimeout) + response, err := w.wh.SendMessage(ReceiveReplyTag, data) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveReplyTag, err) + } + + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveReplyTag, err) + return 0 } - return 0 + return uuid } func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname, @@ -208,27 +184,19 @@ func (w *wasmModel) ReceiveReaction(messageID, reactionTo message.ID, nickname, return 0 } - uuidChan := make(chan uint64) - w.wh.SendMessage(ReceiveReactionTag, data, func(data []byte) { - var uuid uint64 - err = json.Unmarshal(data, &uuid) - if err != nil { - jww.ERROR.Printf( - "Could not JSON unmarshal response to ReceiveReaction: %+v", err) - uuidChan <- 0 - } - uuidChan <- uuid - }) - - select { - case uuid := <-uuidChan: - return uuid - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about ReceiveReaction", worker.ResponseTimeout) + response, err := w.wh.SendMessage(ReceiveReactionTag, data) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", ReceiveReactionTag, err) + } + + var uuid uint64 + if err = json.Unmarshal(response, &uuid); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal UUID from worker for "+ + "%q: %+v", ReceiveReactionTag, err) + return 0 } - return 0 + return uuid } func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID, @@ -247,59 +215,63 @@ func (w *wasmModel) UpdateSentStatus(uuid uint64, messageID message.ID, "Could not JSON marshal payload for TransferMessage: %+v", err) } - w.wh.SendMessage(UpdateSentStatusTag, data, nil) + if err = w.wh.SendNoResponse(UpdateSentStatusTag, data); err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", UpdateSentStatusTag, err) + } } -func (w *wasmModel) BlockSender(senderPubKey ed25519.PublicKey) { - w.wh.SendMessage(BlockSenderTag, senderPubKey, nil) -} +func (w *wasmModel) DeleteMessage( + messageID message.ID, senderPubKey ed25519.PublicKey) bool { + msg := TransferMessage{ + MessageID: messageID, + SenderKey: senderPubKey, + } + + data, err := json.Marshal(msg) + if err != nil { + jww.ERROR.Printf( + "Could not JSON marshal payload for TransferMessage: %+v", err) + } -func (w *wasmModel) UnblockSender(senderPubKey ed25519.PublicKey) { - w.wh.SendMessage(UnblockSenderTag, senderPubKey, nil) + response, err := w.wh.SendMessage(DeleteMessageTag, data) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", DeleteMessageTag, err) + } else if len(response) == 0 { + jww.FATAL.Panicf( + "[DM] Received empty response from %q", DeleteMessageTag) + } + + return response[0] == 1 } func (w *wasmModel) GetConversation(senderPubKey ed25519.PublicKey) *dm.ModelConversation { - resultChan := make(chan *dm.ModelConversation) - w.wh.SendMessage(GetConversationTag, senderPubKey, - func(data []byte) { - var result *dm.ModelConversation - err := json.Unmarshal(data, &result) - if err != nil { - jww.ERROR.Printf("Could not JSON unmarshal response to "+ - "GetConversation: %+v", err) - } - resultChan <- result - }) - - select { - case result := <-resultChan: - return result - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about GetConversation", worker.ResponseTimeout) + response, err := w.wh.SendMessage(GetConversationTag, senderPubKey) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", GetConversationTag, err) + } + + var result dm.ModelConversation + if err = json.Unmarshal(response, &result); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal %T from worker for "+ + "%q: %+v", result, GetConversationTag, err) return nil } + + return &result } func (w *wasmModel) GetConversations() []dm.ModelConversation { - resultChan := make(chan []dm.ModelConversation) - w.wh.SendMessage(GetConversationTag, nil, - func(data []byte) { - var result []dm.ModelConversation - err := json.Unmarshal(data, &result) - if err != nil { - jww.ERROR.Printf("Could not JSON unmarshal response to "+ - "GetConversations: %+v", err) - } - resultChan <- result - }) - - select { - case result := <-resultChan: - return result - case <-time.After(worker.ResponseTimeout): - jww.ERROR.Printf("Timed out after %s waiting for response from the "+ - "worker about GetConversations", worker.ResponseTimeout) + response, err := w.wh.SendMessage(GetConversationsTag, nil) + if err != nil { + jww.FATAL.Panicf("[DM] Failed to send to %q: %+v", GetConversationsTag, err) + } + + var result []dm.ModelConversation + if err = json.Unmarshal(response, &result); err != nil { + jww.ERROR.Printf("[DM] Failed to JSON unmarshal %T from worker for "+ + "%q: %+v", result, GetConversationsTag, err) return nil } + + return result } diff --git a/indexedDb/worker/dm/init.go b/indexedDb/worker/dm/init.go index 3fd1cd13897bdc8684e2efc13750e1fed20ff000..4e084aa29f92f71d296ae85ee39187f38aacc3b8 100644 --- a/indexedDb/worker/dm/init.go +++ b/indexedDb/worker/dm/init.go @@ -12,13 +12,14 @@ package dm import ( "crypto/ed25519" "encoding/json" - "time" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/v4/bindings" "gitlab.com/elixxir/client/v4/dm" - cryptoChannel "gitlab.com/elixxir/crypto/channel" + idbCrypto "gitlab.com/elixxir/crypto/indexedDb" + "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/storage" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -42,8 +43,8 @@ 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, - cb MessageReceivedCallback) (dm.EventModel, error) { +func NewWASMEventModel(path, wasmJsPath string, encryption idbCrypto.Cipher, + cbs bindings.DmCallbacks) (dm.EventModel, error) { databaseName := path + databaseSuffix wh, err := worker.NewManager(wasmJsPath, "dmIndexedDb", true) @@ -52,8 +53,16 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, } // Register handler to manage messages for the MessageReceivedCallback - wh.RegisterCallback( - MessageReceivedCallbackTag, messageReceivedCallbackHandler(cb)) + wh.RegisterCallback(EventUpdateCallbackTag, eventUpdateCallbackHandler(cbs)) + + // Create MessageChannel between worker and logger so that the worker logs + // are saved + err = worker.CreateMessageChannel(logging.GetLogger().Worker(), wh, + "dmIndexedDbLogger", worker.LoggerTag) + if err != nil { + return nil, errors.Wrap(err, "Failed to create message channel "+ + "between DM indexedDb worker and logger") + } // Store the database name err = storage.StoreIndexedDb(databaseName) @@ -83,44 +92,37 @@ func NewWASMEventModel(path, wasmJsPath string, encryption cryptoChannel.Cipher, return nil, err } - dataChan := make(chan []byte) - wh.SendMessage(NewWASMEventModelTag, 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) + response, err := wh.SendMessage(NewWASMEventModelTag, payload) + if err != nil { + return nil, errors.Wrapf(err, + "failed to send message %q", NewWASMEventModelTag) + } else if len(response) > 0 { + return nil, errors.New(string(response)) } return &wasmModel{wh}, nil } -// MessageReceivedCallbackMessage is JSON marshalled and received from the -// worker for the [MessageReceivedCallback] callback. -type MessageReceivedCallbackMessage struct { - UUID uint64 `json:"uuid"` - PubKey ed25519.PublicKey `json:"pubKey"` - MessageUpdate bool `json:"message_update"` - ConversationUpdate bool `json:"conversation_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 "+ - "MessageReceivedCallback message from worker: %+v", err) +// eventUpdateCallbackHandler returns a handler to manage messages for the +// [bindings.DmCallbacks.EventUpdate] callback. +func eventUpdateCallbackHandler( + cbs bindings.DmCallbacks) worker.ReceiverCallback { + return func(message []byte, _ func([]byte)) { + var msg EventUpdateCallbackMessage + if err := json.Unmarshal(message, &msg); err != nil { + jww.ERROR.Printf( + "Failed to JSON unmarshal %T from worker: %+v", msg, err) return } - cb(msg.UUID, msg.PubKey, msg.MessageUpdate, msg.ConversationUpdate) + + cbs.EventUpdate(msg.EventType, msg.JsonData) } } diff --git a/indexedDb/worker/dm/tags.go b/indexedDb/worker/dm/tags.go index b71762421a15003123071954312e290e5677a24e..6679c9be4bd72026a8baf432fb43347dccc7bb53 100644 --- a/indexedDb/worker/dm/tags.go +++ b/indexedDb/worker/dm/tags.go @@ -14,17 +14,16 @@ 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" + NewWASMEventModelTag worker.Tag = "NewWASMEventModel" + EventUpdateCallbackTag worker.Tag = "EventUpdateCallback" ReceiveReplyTag worker.Tag = "ReceiveReply" ReceiveReactionTag worker.Tag = "ReceiveReaction" ReceiveTag worker.Tag = "Receive" ReceiveTextTag worker.Tag = "ReceiveText" - UpdateSentStatusTag worker.Tag = "UpdateSentStatusTag" + UpdateSentStatusTag worker.Tag = "UpdateSentStatus" + DeleteMessageTag worker.Tag = "DeleteMessage" - BlockSenderTag worker.Tag = "BlockSender" - UnblockSenderTag worker.Tag = "UnblockSender" GetConversationTag worker.Tag = "GetConversation" GetConversationsTag worker.Tag = "GetConversations" ) diff --git a/indexedDb/worker/state/implementation.go b/indexedDb/worker/state/implementation.go new file mode 100644 index 0000000000000000000000000000000000000000..17eb1228a873a54131f6a48dedbcd2afc4c312a0 --- /dev/null +++ b/indexedDb/worker/state/implementation.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" + + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + + "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) + } + + response, err := w.wh.SendMessage(SetTag, data) + if err != nil { + jww.FATAL.Panicf("Failed to send message to %q: %+v", SetTag, err) + } else if len(response) > 0 { + return errors.New(string(response)) + } + + return nil +} + +func (w *wasmModel) Get(key string) ([]byte, error) { + + response, err := w.wh.SendMessage(GetTag, []byte(key)) + if err != nil { + jww.FATAL.Panicf("Failed to send message to %q: %+v", GetTag, err) + } + + var msg TransferMessage + if err = json.Unmarshal(response, &msg); err != nil { + return nil, errors.Errorf( + "failed to JSON unmarshal %T from worker: %+v", msg, err) + } + + if len(msg.Error) > 0 { + return nil, errors.New(msg.Error) + } + + return msg.Value, nil +} diff --git a/indexedDb/worker/state/init.go b/indexedDb/worker/state/init.go new file mode 100644 index 0000000000000000000000000000000000000000..4477924c77f3a35e27be1178cb15cfa0957455d6 --- /dev/null +++ b/indexedDb/worker/state/init.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" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/xxdk-wasm/indexedDb/impl" + "gitlab.com/elixxir/xxdk-wasm/logging" + "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"` +} + +// WebState defines an interface for setting persistent state in a KV format +// specifically for web-based implementations. +type WebState interface { + Get(key string) ([]byte, error) + Set(key string, value []byte) error +} + +// 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) (impl.WebState, error) { + databaseName := path + databaseSuffix + + wh, err := worker.NewManager(wasmJsPath, "stateIndexedDb", true) + if err != nil { + return nil, err + } + + // Create MessageChannel between worker and logger so that the worker logs + // are saved + err = worker.CreateMessageChannel(logging.GetLogger().Worker(), wh, + "stateIndexedDbLogger", worker.LoggerTag) + if err != nil { + return nil, errors.Wrap(err, "Failed to create message channel "+ + "between state indexedDb worker and logger") + } + + // 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 + } + + response, err := wh.SendMessage(NewStateTag, payload) + if err != nil { + jww.FATAL.Panicf("Failed to send message to %q: %+v", NewStateTag, err) + } else if len(response) > 0 { + return nil, errors.New(string(response)) + } + + 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/console.go b/logging/console.go index 8c2edd821210d338f73ff18bfabedfc2f319d7d9..bfc249d1f3284ef856c2692060284c5493a0d900 100644 --- a/logging/console.go +++ b/logging/console.go @@ -10,9 +10,10 @@ package logging import ( - jww "github.com/spf13/jwalterweatherman" "io" "syscall/js" + + jww "github.com/spf13/jwalterweatherman" ) var consoleObj = js.Global().Get("console") diff --git a/logging/fileLogger_test.go b/logging/fileLogger_test.go index 317f69544a927280827aa4f3739c7f1e39d30ac4..c28e89cc7a799d0750548e9b84353d21fc551ff4 100644 --- a/logging/fileLogger_test.go +++ b/logging/fileLogger_test.go @@ -11,11 +11,12 @@ package logging import ( "bytes" - "github.com/armon/circbuf" - jww "github.com/spf13/jwalterweatherman" "math/rand" "reflect" "testing" + + "github.com/armon/circbuf" + jww "github.com/spf13/jwalterweatherman" ) func Test_newFileLogger(t *testing.T) { diff --git a/logging/jwwListeners.go b/logging/jwwListeners.go deleted file mode 100644 index 7d83f3d5bcc69190c505dfd0d0676fbe6104d1d0..0000000000000000000000000000000000000000 --- a/logging/jwwListeners.go +++ /dev/null @@ -1,78 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -package logging - -import ( - jww "github.com/spf13/jwalterweatherman" - "sync" -) - -// logListeners contains a map of all registered log listeners keyed on a unique -// ID that can be used to remove the listener once it has been added. This -// global keeps track of all listeners that are registered to jwalterweatherman -// logging. -var logListeners = newLogListenerList() - -type logListenerList struct { - listeners map[uint64]jww.LogListener - currentID uint64 - sync.Mutex -} - -func newLogListenerList() logListenerList { - return logListenerList{ - listeners: make(map[uint64]jww.LogListener), - currentID: 0, - } -} - -// AddLogListener registers the log listener with jwalterweatherman. Returns a -// unique ID that can be used to remove the listener. -func AddLogListener(ll jww.LogListener) uint64 { - logListeners.Lock() - defer logListeners.Unlock() - - id := logListeners.addLogListener(ll) - jww.SetLogListeners(logListeners.mapToSlice()...) - return id -} - -// RemoveLogListener unregisters the log listener with the ID from -// jwalterweatherman. -func RemoveLogListener(id uint64) { - logListeners.Lock() - defer logListeners.Unlock() - - logListeners.removeLogListener(id) - jww.SetLogListeners(logListeners.mapToSlice()...) - -} - -// addLogListener adds the listener to the list and returns its unique ID. -func (lll *logListenerList) addLogListener(ll jww.LogListener) uint64 { - id := lll.currentID - lll.currentID++ - lll.listeners[id] = ll - - return id -} - -// removeLogListener removes the listener with the specified ID from the list. -func (lll *logListenerList) removeLogListener(id uint64) { - delete(lll.listeners, id) -} - -// mapToSlice converts the map of listeners to a slice of listeners so that it -// can be registered with jwalterweatherman.SetLogListeners. -func (lll *logListenerList) mapToSlice() []jww.LogListener { - listeners := make([]jww.LogListener, 0, len(lll.listeners)) - for _, l := range lll.listeners { - listeners = append(listeners, l) - } - return listeners -} diff --git a/logging/logger.go b/logging/logger.go index 3064a00d551a464a486c8f8a23925cf74cea7998..81197d22f98ec09c640f9d1b5fbdd42b9181f926 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -15,7 +15,7 @@ import ( jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/wasm-utils/utils" "gitlab.com/elixxir/xxdk-wasm/worker" ) @@ -26,6 +26,7 @@ const ( WriteLogTag worker.Tag = "WriteLog" GetFileTag worker.Tag = "GetFile" GetFileExtTag worker.Tag = "GetFileExt" + MaxSizeTag worker.Tag = "MaxSize" SizeTag worker.Tag = "Size" ) @@ -38,6 +39,7 @@ func GetLogger() Logger { return logger } +// Logger controls and accesses the log file for this binary. type Logger interface { // StopLogging stops log message writes. Once logging is stopped, it cannot // be resumed and the log file cannot be recovered. @@ -65,6 +67,21 @@ type Logger interface { // worker file buffer. This must be called only once at initialisation. func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int, workerScriptURL, workerName string) error { + return enableLogging(logLevel, fileLogLevel, maxLogFileSizeMB, + workerScriptURL, workerName, js.Undefined()) +} + +// EnableThreadLogging enables logging to the Javascript console and to +// a local or remote thread file buffer. This must be called only once at +// initialisation. +func EnableThreadLogging(logLevel, fileLogLevel jww.Threshold, + maxLogFileSizeMB int, workerName string, messagePort js.Value) error { + return enableLogging( + logLevel, fileLogLevel, maxLogFileSizeMB, "", workerName, messagePort) +} + +func enableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int, + workerScriptURL, workerName string, messagePort js.Value) error { var listeners []jww.LogListener if logLevel > -1 { @@ -80,13 +97,7 @@ func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int, 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 { + if workerScriptURL != "" { wl, err := newWorkerLogger( fileLogLevel, maxLogFileSize, workerScriptURL, workerName) if err != nil { @@ -94,6 +105,18 @@ func EnableLogging(logLevel, fileLogLevel jww.Threshold, maxLogFileSizeMB int, } listeners = append(listeners, wl.Listen) + } else if !messagePort.IsUndefined() { + tl, err := newThreadLogger(fileLogLevel, workerName, messagePort) + if err != nil { + return errors.Wrap(err, "could not initialize logging on message port") + } + listeners = append(listeners, tl.Listen) + } else { + fl, err := newFileLogger(fileLogLevel, maxLogFileSize) + if err != nil { + return errors.Wrap(err, "could not initialize logging to file") + } + listeners = append(listeners, fl.Listen) } js.Global().Set("GetLogger", js.FuncOf(GetLoggerJS)) @@ -121,6 +144,7 @@ func GetLoggerJS(js.Value, []js.Value) any { return newLoggerJS(LoggerJS{GetLogger()}) } +// LoggerJS is the Javascript wrapper for the Logger. type LoggerJS struct { api Logger } diff --git a/logging/workerLogger.go b/logging/workerLogger.go index bfac4703943c94956a8dc6501b722e7d0e5cad62..41f95f7490a292fa459ecb4e6476592280bdc376 100644 --- a/logging/workerLogger.go +++ b/logging/workerLogger.go @@ -14,7 +14,6 @@ import ( "encoding/json" "io" "math" - "time" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -22,8 +21,6 @@ import ( "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 { @@ -52,7 +49,7 @@ func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int, // 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) { + wl.wm.RegisterCallback(GetFileExtTag, func([]byte, func([]byte)) { jww.DEBUG.Print("[LOG] Received file requested from external " + "Javascript. Ignoring file.") }) @@ -63,24 +60,12 @@ func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int, } // 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) + response, err := wl.wm.SendMessage(NewLogFileTag, data) + if err != nil { + return nil, errors.Wrap(err, "failed to initialize the log file listener") + } else if response != nil { + return nil, errors.Wrap(errors.New(string(response)), + "failed to initialize the log file listener") } jww.FEEDBACK.Printf("[LOG] Outputting log to file of max size %d at level "+ @@ -91,11 +76,9 @@ func newWorkerLogger(threshold jww.Threshold, maxLogFileSize int, } // 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. +// worker to be added to the file buffer. Always returns the length of p. func (wl *workerLogger) Write(p []byte) (n int, err error) { - wl.wm.SendMessage(WriteLogTag, p, nil) - return len(p), nil + return len(p), wl.wm.SendNoResponse(WriteLogTag, p) } // Listen adheres to the [jwalterweatherman.LogListener] type and returns the @@ -112,23 +95,22 @@ func (wl *workerLogger) Listen(threshold jww.Threshold) io.Writer { func (wl *workerLogger) StopLogging() { wl.threshold = math.MaxInt - wl.wm.Stop() - jww.DEBUG.Printf("[LOG] Terminated log worker.") + err := wl.wm.Stop() + if err != nil { + jww.ERROR.Printf("[LOG] Failed to terminate log worker: %+v", err) + } else { + 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 + response, err := wl.wm.SendMessage(GetFileTag, nil) + if err != nil { + jww.FATAL.Panicf("[LOG] Failed to get log file from worker: %+v", err) } + + return response } // Threshold returns the log level threshold used in the file. @@ -143,17 +125,12 @@ func (wl *workerLogger) MaxSize() int { // 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 + response, err := wl.wm.SendMessage(SizeTag, nil) + if err != nil { + jww.FATAL.Panicf("[LOG] Failed to get log size from worker: %+v", err) } + + return int(binary.LittleEndian.Uint64(response)) } // Worker returns the manager for the Javascript Worker object. diff --git a/logging/workerThread/main.go b/logging/workerThread/main.go index 1a9a31a0ca39ee684b4427eef03fc89f4a407b8c..0c969963651354d5776c98b48ce1149814595953 100644 --- a/logging/workerThread/main.go +++ b/logging/workerThread/main.go @@ -17,7 +17,7 @@ import ( "syscall/js" "github.com/armon/circbuf" - "github.com/pkg/errors" + "github.com/hack-pad/safejs" "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" @@ -31,8 +31,8 @@ const SEMVER = "0.1.0" // workerLogFile manages communication with the main thread and writing incoming // logging messages to the log file. type workerLogFile struct { - wtm *worker.ThreadManager - b *circbuf.Buffer + tm *worker.ThreadManager + b *circbuf.Buffer } func main() { @@ -64,11 +64,15 @@ var LoggerCmd = &cobra.Command{ jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.") - wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)} + tm, err := worker.NewThreadManager("Logger", true) + if err != nil { + jww.FATAL.Panicf("Failed to get new thread manager: %+v", err) + } + wlf := workerLogFile{tm: tm} wlf.registerCallbacks() - wlf.wtm.SignalReady() + wlf.tm.SignalReady() // Indicate to the Javascript caller that the WASM is ready by resolving // a promise created by the caller. @@ -96,54 +100,96 @@ func init() { // to get the file and file metadata. func (wlf *workerLogFile) registerCallbacks() { // Callback for logging.LogToFileWorker - wlf.wtm.RegisterCallback(logging.NewLogFileTag, - func(data []byte) ([]byte, error) { + wlf.tm.RegisterCallback(logging.NewLogFileTag, + func(message []byte, reply func([]byte)) { + var maxLogFileSize int64 - err := json.Unmarshal(data, &maxLogFileSize) + err := json.Unmarshal(message, &maxLogFileSize) if err != nil { - return []byte(err.Error()), err + reply([]byte(err.Error())) + return } wlf.b, err = circbuf.NewBuffer(maxLogFileSize) if err != nil { - return []byte(err.Error()), err + reply([]byte(err.Error())) + return } jww.DEBUG.Printf("[LOG] Created new worker log file of size %d", maxLogFileSize) - return []byte{}, nil + reply(nil) }) // Callback for Logging.GetFile - wlf.wtm.RegisterCallback(logging.WriteLogTag, - func(data []byte) ([]byte, error) { - n, err := wlf.b.Write(data) + wlf.tm.RegisterCallback(logging.WriteLogTag, + func(message []byte, _ func([]byte)) { + n, err := wlf.b.Write(message) if err != nil { - return nil, err - } else if n != len(data) { - return nil, errors.Errorf( - "wrote %d bytes; expected %d bytes", n, len(data)) + jww.ERROR.Printf("[LOG] Failed to write to log: %+v", err) + } else if n != len(message) { + jww.ERROR.Printf("[LOG] Failed to write to log: wrote %d "+ + "bytes; expected %d bytes", n, len(message)) } - - return nil, nil }, ) // Callback for Logging.GetFile - wlf.wtm.RegisterCallback(logging.GetFileTag, func([]byte) ([]byte, error) { - return wlf.b.Bytes(), nil + wlf.tm.RegisterCallback(logging.GetFileTag, + func(_ []byte, reply func([]byte)) { reply(wlf.b.Bytes()) }) + + // Callback for Logging.GetFile + wlf.tm.RegisterCallback(logging.GetFileExtTag, + func(_ []byte, reply func([]byte)) { reply(wlf.b.Bytes()) }) + + // Callback for Logging.Size + wlf.tm.RegisterCallback(logging.SizeTag, func(_ []byte, reply func([]byte)) { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(wlf.b.TotalWritten())) + reply(b) }) + wlf.tm.RegisterMessageChannelCallback(worker.LoggerTag, wlf.registerLogWorker) +} + +func (wlf *workerLogFile) registerLogWorker(port js.Value, channelName string) { + p := worker.DefaultParams() + p.MessageLogging = false + mm, err := worker.NewMessageManager( + safejs.Safe(port), channelName+"-logger", p) + if err != nil { + jww.FATAL.Panic(err) + } + + mm.RegisterCallback(logging.WriteLogTag, + func(message []byte, _ func([]byte)) { + n, err := wlf.b.Write(message) + if err != nil { + jww.ERROR.Printf("[LOG] Failed to write to log: %+v", err) + } else if n != len(message) { + jww.ERROR.Printf("[LOG] Failed to write to log: wrote %d "+ + "bytes; expected %d bytes", n, len(message)) + } + }, + ) + // Callback for Logging.GetFile - wlf.wtm.RegisterCallback(logging.GetFileExtTag, func([]byte) ([]byte, error) { - return wlf.b.Bytes(), nil + mm.RegisterCallback(logging.GetFileTag, func(_ []byte, reply func([]byte)) { + reply(wlf.b.Bytes()) + }) + + // Callback for Logging.MaxSize + mm.RegisterCallback(logging.GetFileTag, func(_ []byte, reply func([]byte)) { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(wlf.b.Size())) + reply(b) }) // Callback for Logging.Size - wlf.wtm.RegisterCallback(logging.SizeTag, func([]byte) ([]byte, error) { + mm.RegisterCallback(logging.SizeTag, func(_ []byte, reply func([]byte)) { b := make([]byte, 8) binary.LittleEndian.PutUint64(b, uint64(wlf.b.TotalWritten())) - return b, nil + reply(b) }) } diff --git a/logging/workerThreadLogger.go b/logging/workerThreadLogger.go new file mode 100644 index 0000000000000000000000000000000000000000..900e82de69e4c3a3397c485c197cb9b093824d69 --- /dev/null +++ b/logging/workerThreadLogger.go @@ -0,0 +1,117 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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" + "io" + "math" + "syscall/js" + + "github.com/hack-pad/safejs" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/xxdk-wasm/worker" +) + +// threadLogger manages the recording of jwalterweatherman logs from a worker to +// the in-memory file buffer in a remote Worker thread. +type threadLogger struct { + threshold jww.Threshold + mm *worker.MessageManager +} + +// newThreadLogger starts logging to an in-memory log file in a remote Worker +// at the specified threshold. Returns a [threadLogger] that can be used to get +// the log file. +func newThreadLogger(threshold jww.Threshold, channelName string, + messagePort js.Value) (*threadLogger, error) { + p := worker.DefaultParams() + p.MessageLogging = false + mm, err := worker.NewMessageManager(safejs.Safe(messagePort), + channelName+"-worker", p) + if err != nil { + return nil, err + } + + tl := &threadLogger{ + threshold: threshold, + mm: mm, + } + + jww.FEEDBACK.Printf("[LOG] Worker outputting log to file at level "+ + "%s using web worker", tl.threshold) + + logger = tl + return tl, 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 (tl *threadLogger) Write(p []byte) (n int, err error) { + return len(p), tl.mm.SendNoResponse(WriteLogTag, p) +} + +// Listen adheres to the [jwalterweatherman.LogListener] type and returns the +// log writer when the threshold is within the set threshold limit. +func (tl *threadLogger) Listen(threshold jww.Threshold) io.Writer { + if threshold < tl.threshold { + return nil + } + return tl +} + +// StopLogging stops sending log messages to the logging worker. Once logging is +// stopped, it cannot be resumed and the log file cannot be recovered. This does +// not stop the logging worker. +func (tl *threadLogger) StopLogging() { + tl.threshold = math.MaxInt +} + +// GetFile returns the entire log file. +func (tl *threadLogger) GetFile() []byte { + response, err := tl.mm.Send(GetFileTag, nil) + if err != nil { + jww.FATAL.Panicf("[LOG] Failed to get log file from worker: %+v", err) + } + + return response +} + +// Threshold returns the log level threshold of logs sent to the worker. +func (tl *threadLogger) Threshold() jww.Threshold { + return tl.threshold +} + +// MaxSize returns the max size, in bytes, that the log file is allowed to be. +func (tl *threadLogger) MaxSize() int { + response, err := tl.mm.Send(MaxSizeTag, nil) + if err != nil { + jww.FATAL.Panicf("[LOG] Failed to max file size from worker: %+v", err) + } + + return int(binary.LittleEndian.Uint64(response)) +} + +// Size returns the number of bytes written to the log file. +func (tl *threadLogger) Size() int { + response, err := tl.mm.Send(SizeTag, nil) + if err != nil { + jww.FATAL.Panicf("[LOG] Failed to file size from worker: %+v", err) + } + + return int(binary.LittleEndian.Uint64(response)) +} + +// Worker always returns nil +func (tl *threadLogger) Worker() *worker.Manager { + return nil +} diff --git a/main.go b/main.go index 46cdc133897c1225c9956ea44c5cc2d3018b70aa..470740cfec9e85dda4eedb5de36b32854b26e27f 100644 --- a/main.go +++ b/main.go @@ -11,15 +11,16 @@ package main import ( "fmt" - "github.com/spf13/cobra" "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" ) @@ -99,6 +100,11 @@ func setGlobals() { 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)) @@ -121,17 +127,24 @@ func setGlobals() { js.FuncOf(wasm.NewChannelsManagerWithIndexedDbUnsafe)) js.Global().Set("DecodePublicURL", js.FuncOf(wasm.DecodePublicURL)) js.Global().Set("DecodePrivateURL", js.FuncOf(wasm.DecodePrivateURL)) + js.Global().Set("DecodeInviteURL", js.FuncOf(wasm.DecodeInviteURL)) js.Global().Set("GetChannelJSON", js.FuncOf(wasm.GetChannelJSON)) js.Global().Set("GetChannelInfo", js.FuncOf(wasm.GetChannelInfo)) 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("GetChannelNotificationReportsForMe", + js.FuncOf(wasm.GetChannelNotificationReportsForMe)) 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.GetChannelNotificationReportsForMe)) - // wasm/dm.go + // wasm/cipher.go + js.Global().Set("NewDatabaseCipher", + js.FuncOf(wasm.NewDatabaseCipher)) + + // wasm/channelsFileTransfer.go js.Global().Set("InitChannelsFileTransfer", js.FuncOf(wasm.InitChannelsFileTransfer)) @@ -141,12 +154,18 @@ func setGlobals() { js.FuncOf(wasm.NewDMClientWithIndexedDb)) js.Global().Set("NewDMClientWithIndexedDbUnsafe", js.FuncOf(wasm.NewDMClientWithIndexedDbUnsafe)) - js.Global().Set("NewDMsDatabaseCipher", - js.FuncOf(wasm.NewDMsDatabaseCipher)) + js.Global().Set("NewDMsDatabaseCipher", js.FuncOf(wasm.NewDatabaseCipher)) + js.Global().Set("DecodeDMShareURL", js.FuncOf(wasm.DecodeDMShareURL)) + js.Global().Set("GetDmNotificationReportsForMe", + js.FuncOf(wasm.GetDmNotificationReportsForMe)) // wasm/cmix.go js.Global().Set("NewCmix", js.FuncOf(wasm.NewCmix)) + js.Global().Set("NewSynchronizedCmix", + js.FuncOf(wasm.NewSynchronizedCmix)) 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)) @@ -230,6 +249,8 @@ func setGlobals() { 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)) @@ -252,7 +273,7 @@ func setGlobals() { var ( logLevel, fileLogLevel jww.Threshold - maxLogFileSizeMB int + maxLogFileSizeMB int workerScriptURL, workerName string ) 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..ecacf52e5d3f77c1d1aa16847b2f778c5fd88d5e 100644 --- a/storage/password.go +++ b/storage/password.go @@ -12,16 +12,22 @@ 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/crypto/hash" + "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. @@ -35,6 +41,8 @@ const ( // saltLen is the length of the salt. Recommended to be 16 bytes here: // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-argon2-04#section-3.1 saltLen = 16 + + internalPasswordConstant = "XXInternalPassword" ) // Storage keys. @@ -53,7 +61,7 @@ const ( // Error messages. const ( // initInternalPassword - readInternalPasswordErr = "could not generate internal password: %+v" + readInternalPasswordErr = "could not generate" internalPasswordNumBytesErr = "expected %d bytes for internal password, found %d bytes" // getInternalPassword @@ -85,17 +93,21 @@ const ( // Parameters: // - args[0] - The user supplied password (string). // -// Returns: +// Returns a promise: // - Internal password (Uint8Array). // - Throws TypeError on failure. func GetOrInitPassword(_ js.Value, args []js.Value) any { - internalPassword, err := getOrInit(args[0].String()) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil + externalPassword := args[0].String() + promiseFn := func(resolve, reject func(args ...any) js.Value) { + internalPassword, err := getOrInit(externalPassword) + if err != nil { + reject(exception.NewTrace(err)) + } else { + resolve(utils.CopyBytesToJS(internalPassword)) + } } - return utils.CopyBytesToJS(internalPassword) + return utils.CreatePromise(promiseFn) } // ChangeExternalPassword allows a user to change their external password. @@ -109,7 +121,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 +142,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 +160,10 @@ 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() + // NOTE: the following no longer works in synchronized environments, so + // disabled in produciton. + jww.FATAL.Panicf("cannot change password, unimplemented") + localStorage := storage.GetLocalStorage() internalPassword, err := getInternalPassword( oldExternalPassword, localStorage) if err != nil { @@ -159,13 +174,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,44 +192,61 @@ 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) + // FIXME: The internal password is now just an expansion of + // the users password text. We couldn't preserve the following + // when doing cross-device sync. + h := hash.CMixHash.New() + h.Write([]byte(externalPassword)) + h.Write(internalPassword) + copy(internalPassword, h.Sum(nil)[:internalPasswordLen]) + // Generate internal password - n, err := csprng.Read(internalPassword) - if err != nil { - return nil, errors.Errorf(readInternalPasswordErr, err) - } else if n != internalPasswordLen { - return nil, errors.Errorf( - internalPasswordNumBytesErr, internalPasswordLen, n) - } + // n, err := csprng.Read(internalPassword) + // if err != nil { + // return nil, errors.Errorf(readInternalPasswordErr, err) + // } else if n != internalPasswordLen { + // return nil, errors.Errorf( + // internalPasswordNumBytesErr, internalPasswordLen, n) + // } // Generate and store salt salt, err := makeSalt(csprng) 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 +254,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..fecd785c51c7e919ab511e2d8d5821dd6f4b37f5 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 @@ -42,42 +44,42 @@ func Test_getOrInit(t *testing.T) { // Tests that changeExternalPassword correctly changes the password and updates // the encryption. -func Test_changeExternalPassword(t *testing.T) { - oldExternalPassword := "myPassword" - newExternalPassword := "hunter2" - oldInternalPassword, err := getOrInit(oldExternalPassword) - if err != nil { - t.Errorf("%+v", err) - } - - err = changeExternalPassword(oldExternalPassword, newExternalPassword) - if err != nil { - t.Errorf("%+v", err) - } - - newInternalPassword, err := getOrInit(newExternalPassword) - if err != nil { - t.Errorf("%+v", err) - } - - if !bytes.Equal(oldInternalPassword, newInternalPassword) { - t.Errorf("Internal password was not changed in storage. Old and new "+ - "should be different.\nold: %+v\nnew: %+v", - oldInternalPassword, newInternalPassword) - } - - _, err = getOrInit(oldExternalPassword) - expectedErr := strings.Split(decryptWithPasswordErr, "%")[0] - if err == nil || !strings.Contains(err.Error(), expectedErr) { - t.Errorf("Unexpected error when trying to get internal password with "+ - "old external password.\nexpected: %s\nreceived: %+v", expectedErr, err) - } -} +// func Test_changeExternalPassword(t *testing.T) { +// oldExternalPassword := "myPassword" +// newExternalPassword := "hunter2" +// oldInternalPassword, err := getOrInit(oldExternalPassword) +// if err != nil { +// t.Errorf("%+v", err) +// } + +// err = changeExternalPassword(oldExternalPassword, newExternalPassword) +// if err != nil { +// t.Errorf("%+v", err) +// } + +// newInternalPassword, err := getOrInit(newExternalPassword) +// if err != nil { +// t.Errorf("%+v", err) +// } + +// if !bytes.Equal(oldInternalPassword, newInternalPassword) { +// t.Errorf("Internal password was not changed in storage. Old and new "+ +// "should be different.\nold: %+v\nnew: %+v", +// oldInternalPassword, newInternalPassword) +// } + +// _, err = getOrInit(oldExternalPassword) +// expectedErr := strings.Split(decryptWithPasswordErr, "%")[0] +// if err == nil || !strings.Contains(err.Error(), expectedErr) { +// t.Errorf("Unexpected error when trying to get internal password with "+ +// "old external password.\nexpected: %s\nreceived: %+v", expectedErr, err) +// } +// } // 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] @@ -152,26 +154,26 @@ func Test_initInternalPassword_CsprngReadError(t *testing.T) { // Tests that initInternalPassword returns an error when the RNG does not // return enough bytes. -func Test_initInternalPassword_CsprngReadNumBytesError(t *testing.T) { - externalPassword := "myPassword" - ls := GetLocalStorage() - b := bytes.NewBuffer(make([]byte, internalPasswordLen/2)) +// func Test_initInternalPassword_CsprngReadNumBytesError(t *testing.T) { +// externalPassword := "myPassword" +// ls := storage.GetLocalStorage() +// b := bytes.NewBuffer(make([]byte, internalPasswordLen/2)) - expectedErr := fmt.Sprintf( - internalPasswordNumBytesErr, internalPasswordLen, internalPasswordLen/2) +// expectedErr := fmt.Sprintf( +// internalPasswordNumBytesErr, internalPasswordLen, internalPasswordLen/2) - _, err := initInternalPassword(externalPassword, ls, b, defaultParams()) - if err == nil || !strings.Contains(err.Error(), expectedErr) { - t.Errorf("Unexpected error when RNG does not return enough bytes."+ - "\nexpected: %s\nreceived: %+v", expectedErr, err) - } -} +// _, err := initInternalPassword(externalPassword, ls, b, defaultParams()) +// if err == nil || !strings.Contains(err.Error(), expectedErr) { +// t.Errorf("Unexpected error when RNG does not return enough bytes."+ +// "\nexpected: %s\nreceived: %+v", expectedErr, err) +// } +// } // Tests that getInternalPassword returns the internal password that is saved // 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..a7b7f64f6dca64d8ef69b2b84cbf976db1569675 100644 --- a/storage/purge.go +++ b/storage/purge.go @@ -10,13 +10,14 @@ package storage import ( - "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" + + "github.com/hack-pad/go-indexeddb/idb" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/storage" ) // numClientsRunning is an atomic that tracks the current number of Cmix @@ -43,36 +44,32 @@ func DecrementNumClientsRunning() { // password is required. // // Parameters: -// - args[0] - Storage directory path (string). This is the same directory -// path passed into [wasm.NewCmix]. -// - args[1] - The user-supplied password (string). This is the same password +// - args[0] - The user-supplied password (string). This is the same password // passed into [wasm.NewCmix]. // // Returns: -// - Throws a TypeError if the password is incorrect or if not all cMix -// followers have been stopped. +// - 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() - userPassword := args[1].String() + userPassword := args[0].String() // 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,28 +79,18 @@ 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) - 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) - jww.DEBUG.Printf("[PURGE] Cleared %d keys with the prefix %q (for NDF)", - n, utility.NdfStorageKeyNamePrefix) - return nil } diff --git a/storage/version.go b/storage/version.go index b0f192ab24f8ad80d1fb9a9a5c96177f67545440..3a35edfd31eda105afa5e1583dcf891b511b62f8 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 8f156761dd4919cfb4e92b93d91acef0253c6901..0000000000000000000000000000000000000000 --- a/utils/utils.go +++ /dev/null @@ -1,108 +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) - var handler js.Func - 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) - go func() { handler.Release() }() - }(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 21ef36e6546871d714be2aba31ec8d223b5e7021..6b99feb119bffe5c79b4715adab08d14ccb7bd45 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,6 +58,8 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any { "SendMessage": js.FuncOf(cm.SendMessage), "SendReply": js.FuncOf(cm.SendReply), "SendReaction": js.FuncOf(cm.SendReaction), + "SendSilent": js.FuncOf(cm.SendSilent), + "SendInvite": js.FuncOf(cm.SendInvite), "DeleteMessage": js.FuncOf(cm.DeleteMessage), "PinMessage": js.FuncOf(cm.PinMessage), "MuteUser": js.FuncOf(cm.MuteUser), @@ -79,6 +79,12 @@ func newChannelsManagerJS(api *bindings.ChannelsManager) map[string]any { // Channel Receiving Logic and Callback Registration "RegisterReceiveHandler": js.FuncOf(cm.RegisterReceiveHandler), + + // Notifications + "GetNotificationLevel": js.FuncOf(cm.GetNotificationLevel), + "GetNotificationStatus": js.FuncOf(cm.GetNotificationStatus), + "SetMobileNotificationsLevel": js.FuncOf( + cm.SetMobileNotificationsLevel), } return channelsManagerMap @@ -105,11 +111,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 } @@ -128,7 +134,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 @@ -144,7 +150,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 } @@ -162,12 +168,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 } @@ -184,14 +190,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 } @@ -206,13 +212,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 } @@ -229,14 +235,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 } @@ -263,20 +269,28 @@ func GetPublicChannelIdentityFromPrivate(_ 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] - 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]) em := newEventModelBuilder(args[2]) extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3]) + notificationsID := args[4].Int() + cUI := newChannelUI(args[5]) cm, err := bindings.NewChannelsManager( - cmixId, privateIdentity, em, extensionBuilderIDsJSON) + cmixId, privateIdentity, em, extensionBuilderIDsJSON, notificationsID, cUI) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -303,19 +317,27 @@ func NewChannelsManager(_ 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]`. +// - 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]) extensionBuilderIDsJSON := utils.CopyBytesToGo(args[3]) + notificationsID := args[4].Int() + cUI := newChannelUI(args[5]) cm, err := bindings.LoadChannelsManager( - cmixID, storageTag, em, extensionBuilderIDsJSON) + cmixID, storageTag, em, extensionBuilderIDsJSON, notificationsID, cUI) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -343,48 +365,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 -// object with [NewChannelsDatabaseCipher] and get its id with -// [ChannelDbCipher.GetID]. +// - 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 [DbCipher] object in tracker (int). Create this +// object with [NewDatabaseCipher] and get its id with +// [DbCipher.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) + cipher, err := dbCipherTrackerSingleton.get(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 @@ -409,22 +419,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. @@ -436,40 +436,26 @@ 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 *DbCipher) any { model := channelsDb.NewWASMEventModelBuilder( - wasmJsPath, cipher, messageReceived, deletedMessage, mutedUser) + wasmJsPath, cipher.api, 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)) } @@ -492,46 +478,40 @@ 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[6] - ID of [ChannelDbCipher] object in tracker (int). Create this -// object with [NewChannelsDatabaseCipher] and get its id with -// [ChannelDbCipher.GetID]. +// - 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 [DbCipher] object in tracker (int). Create this +// object with [NewDatabaseCipher] and get its id with +// [DbCipher.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 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) + cipher, err := dbCipherTrackerSingleton.get(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] @@ -550,22 +530,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. @@ -574,39 +548,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, - 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) - } + extensionBuilderIDsJSON []byte, notificationsID int, + channelsCbs bindings.ChannelUICallbacks, cipher *DbCipher) any { model := channelsDb.NewWASMEventModelBuilder( - wasmJsPath, cipher, messageReceived, deletedMessage, mutedUser) + wasmJsPath, cipher.api, 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)) } @@ -632,7 +594,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 } @@ -654,7 +616,33 @@ 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 + } + + return c +} + +// DecodeInviteURL decodes the channel URL, using the password, into a channel +// pretty print. This function can only be used for URLs from invitations. +// +// Parameters: +// - args[0] - The channel's share URL (string). Should be received from +// another user via invitation. +// - args[1] - The password needed to decrypt the secret data in the URL +// (string). +// +// Returns: +// - The channel pretty print (string) +func DecodeInviteURL(_ js.Value, args []js.Value) any { + var ( + url = args[0].String() + password = args[1].String() + ) + + c, err := bindings.DecodeInviteURL(url, password) + if err != nil { + exception.ThrowTrace(err) return nil } @@ -685,7 +673,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 } @@ -704,11 +692,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 } @@ -756,7 +744,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) } @@ -786,7 +774,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)) } @@ -810,7 +798,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() } @@ -829,13 +817,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 } @@ -846,7 +834,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: // @@ -857,7 +845,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 } @@ -871,12 +859,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 @@ -889,12 +877,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 @@ -907,12 +895,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 @@ -955,7 +943,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() @@ -964,7 +952,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 } @@ -980,7 +968,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: // @@ -990,7 +978,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 } @@ -1034,6 +1022,22 @@ 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 map of slices of [ed25519.PublicKey] of users that +// should receive mobile notifications for the message. Each slice keys on a +// [channels.PingType] that describes the type of notification it is +// (Uint8Array). +// +// Example map of slices of public keys: +// +// { +// "usrMention": [ +// "CLdKxbe8D2WVOpx1mT63TZ5CP/nesmxHLT5DUUalpe0=", +// "S2c6NXjNqgR11SCOaiQUughWaLpWBKNufPt6cbTVHMA=" +// ], +// "usrReply": [ +// "aaMzSeA6Cu2Aix2MlOwzrAI+NnpKshzvZRT02PZPVec=" +// ] +// } // // Returns a promise: // - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). @@ -1045,12 +1049,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]) + pingsMapJSON := utils.CopyBytesToGo(args[6]) promiseFn := func(resolve, reject func(args ...any) js.Value) { sendReport, err := cm.api.SendGeneric(marshalledChanId, messageType, - msg, leaseTimeMS, tracked, cmixParamsJSON) + msg, leaseTimeMS, tracked, cmixParamsJSON, pingsMapJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1078,6 +1083,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). @@ -1087,12 +1102,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) { sendReport, err := cm.api.SendMessage( - marshalledChanId, msg, leaseTimeMS, cmixParamsJSON) + marshalledChanId, msg, leaseTimeMS, cmixParamsJSON, pingsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1127,6 +1143,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). @@ -1137,12 +1163,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) { sendReport, err := cm.api.SendReply(marshalledChanId, msg, - messageToReactTo, leaseTimeMS, cmixParamsJSON) + messageToReactTo, leaseTimeMS, cmixParamsJSON, pingsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1190,7 +1217,104 @@ 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)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// SendInvite is used to send to a channel (invited) an invitation to another +// channel (invitee). +// +// If the channel ID for the invitee channel is not recognized by the Manager, +// then an error will be returned. +// +// Parameters: +// - args[0] - Marshalled bytes of the invited channel [id.ID] (Uint8Array). +// - args[1] - JSON of the invitee channel [id.ID]. +// This can be retrieved from [GetChannelJSON]. (Uint8Array). +// - args[2] - The contents of the message (string). +// - args[3] - The URL to append the channel info to (string). +// - args[4] - 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[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). +// - Rejected with an error if sending fails. +func (cm *ChannelsManager) SendInvite(_ js.Value, args []js.Value) any { + var ( + marshalledChanId = utils.CopyBytesToGo(args[0]) + inviteToJSON = utils.CopyBytesToGo(args[1]) + msg = args[2].String() + host = args[3].String() + leaseTimeMS = int64(args[4].Int()) + cmixParamsJSON = utils.CopyBytesToGo(args[5]) + pingsJSON = utils.CopyBytesToGo(args[6]) + ) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := cm.api.SendInvite(marshalledChanId, + inviteToJSON, msg, host, leaseTimeMS, + cmixParamsJSON, pingsJSON) + if err != nil { + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -1247,7 +1371,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)) } @@ -1286,7 +1410,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)) } @@ -1328,7 +1452,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)) } @@ -1369,7 +1493,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)) } @@ -1391,7 +1515,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 } @@ -1410,7 +1534,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 } @@ -1439,7 +1563,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 } @@ -1457,7 +1581,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 } @@ -1476,7 +1600,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 } @@ -1498,7 +1622,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 @@ -1512,13 +1636,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 } @@ -1535,7 +1659,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: // @@ -1544,13 +1668,177 @@ 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 +} + +// GetNotificationStatus returns the notification status for the given channel. +// +// Parameters: +// - args[0] - The marshalled bytes of the channel's [id.ID] (Uint8Array). +// +// Returns: +// - The [notifications.NotificationState] for the channel (int). +// - Throws an error if the channel ID cannot be unmarshalled or the channel +// cannot be found. +func (cm *ChannelsManager) GetNotificationStatus(_ js.Value, args []js.Value) any { + channelIDBytes := utils.CopyBytesToGo(args[0]) + + status, err := cm.api.GetNotificationStatus(channelIDBytes) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + return status +} + +// 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 a promise and 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() + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := cm.api.SetMobileNotificationsLevel(channelIDBytes, + level, status) + if err != nil { + reject(exception.NewTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + +// GetChannelNotificationReportsForMe 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]. +// It can optionally be the entire json return from +// [bindings.NotificationUpdateJson] instead of just the needed subsection +// (Uint8Array). +// - notificationDataCSV - CSV containing notification data (string). +// +// 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":{}} +// } +// } +// ] +// +// Returns: +// - The JSON of a slice of [channels.NotificationReport] (Uint8Array). +// - Throws an error if getting the report fails. +// +// Example return: +// +// [ +// { +// "channel": "jOgZopfYj4zrE/AHtKmkf+QEWnfUKv9KfIy/+Bsg0PkD", +// "type": 1, +// "pingType": "usrMention" +// }, +// { +// "channel": "GKmfN/LKXQYM6++TC6DeZYqoxvSUPkh5UAHWODqh9zkD", +// "type": 2, +// "pingType": "usrReply" +// }, +// { +// "channel": "M+28xtj0coHrhDHfojGNcyb2c4maO7ZuheB6egS0Pc4D", +// "type": 1, +// "pingType": "" +// } +// ] +func GetChannelNotificationReportsForMe(_ js.Value, args []js.Value) any { + notificationFilterJSON := utils.CopyBytesToGo(args[0]) + notificationDataCSV := args[1].String() + + report, err := bindings.GetChannelNotificationReportsForMe( + notificationFilterJSON, notificationDataCSV) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + return utils.CopyBytesToJS(report) +} + //////////////////////////////////////////////////////////////////////////////// // Admin Management // //////////////////////////////////////////////////////////////////////////////// @@ -1563,11 +1851,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 } @@ -1599,12 +1887,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) @@ -1622,14 +1910,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]) @@ -1638,7 +1926,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 } @@ -1656,13 +1944,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]) @@ -1671,7 +1959,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 } @@ -1688,11 +1976,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 } @@ -1722,7 +2010,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() } @@ -1750,7 +2038,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{ @@ -1763,7 +2051,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 } @@ -1795,7 +2083,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. @@ -2144,141 +2432,22 @@ type MessageAndError struct { Error string } -//////////////////////////////////////////////////////////////////////////////// -// Channel Cipher // -//////////////////////////////////////////////////////////////////////////////// - -// ChannelDbCipher wraps the [bindings.ChannelDbCipher] object so its methods -// can be wrapped to be Javascript compatible. -type ChannelDbCipher struct { - api *bindings.ChannelDbCipher -} - -// newChannelDbCipherJS creates a new Javascript compatible object -// (map[string]any) that matches the [ChannelDbCipher] structure. -func newChannelDbCipherJS(api *bindings.ChannelDbCipher) map[string]any { - c := ChannelDbCipher{api} - channelDbCipherMap := map[string]any{ - "GetID": js.FuncOf(c.GetID), - "Encrypt": js.FuncOf(c.Encrypt), - "Decrypt": js.FuncOf(c.Decrypt), - "MarshalJSON": js.FuncOf(c.MarshalJSON), - "UnmarshalJSON": js.FuncOf(c.UnmarshalJSON), - } - - return channelDbCipherMap -} - -// NewChannelsDatabaseCipher constructs a [ChannelDbCipher] object. -// -// Parameters: -// - args[0] - The tracked [Cmix] object ID (int). -// - args[1] - The password for storage. This should be the same password -// passed into [NewCmix] (Uint8Array). -// - args[2] - The maximum size of a payload to be encrypted. A payload passed -// into [ChannelDbCipher.Encrypt] that is larger than this value will result -// in an error (int). -// -// Returns: -// - JavaScript representation of the [ChannelDbCipher] object. -// - Throws a TypeError if creating the cipher fails. -func NewChannelsDatabaseCipher(_ js.Value, args []js.Value) any { - cmixId := args[0].Int() - password := utils.CopyBytesToGo(args[1]) - plaintTextBlockSize := args[2].Int() - - cipher, err := bindings.NewChannelsDatabaseCipher( - cmixId, password, plaintTextBlockSize) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil - } - - return newChannelDbCipherJS(cipher) -} - -// GetID returns the ID for this [bindings.ChannelDbCipher] in the -// channelDbCipherTracker. -// -// Returns: -// - Tracker ID (int). -func (c *ChannelDbCipher) GetID(js.Value, []js.Value) any { - return c.api.GetID() -} - -// Encrypt will encrypt the raw data. It will return a ciphertext. Padding is -// done on the plaintext so all encrypted data looks uniform at rest. -// -// Parameters: -// - args[0] - The data to be encrypted (Uint8Array). This must be smaller -// than the block size passed into [NewChannelsDatabaseCipher]. If it is -// larger, this will return an error. -// -// Returns: -// - The ciphertext of the plaintext passed in (Uint8Array). -// - Throws a TypeError 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) - return nil - } - - return utils.CopyBytesToJS(ciphertext) -} - -// Decrypt will decrypt the passed in encrypted value. The plaintext will be -// returned by this function. Any padding will be discarded within this -// function. -// -// Parameters: -// - args[0] - the encrypted data returned by [ChannelDbCipher.Encrypt] -// (Uint8Array). -// -// Returns: -// - The plaintext of the ciphertext passed in (Uint8Array). -// - Throws a TypeError 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) - 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"), } - - return utils.CopyBytesToJS(plaintext) } -// MarshalJSON marshals the cipher into valid JSON. -// -// Returns: -// - JSON of the cipher (Uint8Array). -// - Throws a TypeError 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) - return nil - } - - return utils.CopyBytesToJS(data) +// eventModel wraps Javascript callbacks to adhere to the +// [bindings.ChannelUICallbacks] interface. +type channelUI struct { + eventUpdate func(args ...any) js.Value } -// UnmarshalJSON unmarshalls JSON into the cipher. -// -// Note that this function does not transfer the internal RNG. Use -// [channel.NewCipherFromJSON] to properly reconstruct a cipher from JSON. -// -// Parameters: -// - args[0] - JSON data to unmarshal (Uint8Array). -// -// Returns: -// - JSON of the cipher (Uint8Array). -// - Throws a TypeError 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) - return nil - } - return nil +// 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..c96b547283a1b7d337666c17ef846370d4344825 100644 --- a/wasm/channelsFileTransfer.go +++ b/wasm/channelsFileTransfer.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" ) // ChannelsFileTransfer wraps the [bindings.ChannelsFileTransfer] object so its @@ -67,7 +69,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 +166,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)) } @@ -192,6 +194,8 @@ func (cft *ChannelsFileTransfer) Upload(_ js.Value, args []js.Value) any { // the channel (int). For the maximum amount of time, use [ValidForever]. // - args[6] - JSON of [xxdk.CMIXParams] (Uint8Array). If left empty, // [GetDefaultCMixParams] will be used internally. +// - args[7] - JSON of a slice of public keys of users that should receive +// mobile notifications for the message. // // Returns a promise: // - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). @@ -205,13 +209,15 @@ func (cft *ChannelsFileTransfer) Send(_ js.Value, args []js.Value) any { preview = utils.CopyBytesToGo(args[4]) validUntilMS = args[5].Int() cmixParamsJSON = utils.CopyBytesToGo(args[6]) + pingsJSON = utils.CopyBytesToGo(args[7]) ) promiseFn := func(resolve, reject func(args ...any) js.Value) { - fileID, err := cft.api.Send(channelIdBytes, fileLinkJSON, fileName, - fileType, preview, validUntilMS, cmixParamsJSON) + fileID, err := cft.api.Send(channelIdBytes, fileLinkJSON, + fileName, fileType, preview, validUntilMS, + cmixParamsJSON, pingsJSON) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(fileID)) } @@ -264,7 +270,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 +311,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 +340,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 +392,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 +444,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 +496,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 +524,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..9ebbc691debd6d330a9cadc3a4aa4c9c62d342ad 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" @@ -60,47 +60,6 @@ func Test_ChannelsManagerMethods(t *testing.T) { } } -// Tests that the map representing ChannelDbCipher returned by -// newChannelDbCipherJS contains all of the methods on ChannelDbCipher. -func Test_newChannelDbCipherJS(t *testing.T) { - cipherType := reflect.TypeOf(&ChannelDbCipher{}) - - cipher := newChannelDbCipherJS(&bindings.ChannelDbCipher{}) - if len(cipher) != cipherType.NumMethod() { - t.Errorf("ChannelDbCipher JS object does not have all methods."+ - "\nexpected: %d\nreceived: %d", cipherType.NumMethod(), len(cipher)) - } - - for i := 0; i < cipherType.NumMethod(); i++ { - method := cipherType.Method(i) - - if _, exists := cipher[method.Name]; !exists { - t.Errorf("Method %s does not exist.", method.Name) - } - } -} - -// Tests that ChannelDbCipher has all the methods that -// [bindings.ChannelDbCipher] has. -func Test_ChannelDbCipherMethods(t *testing.T) { - cipherType := reflect.TypeOf(&ChannelDbCipher{}) - binCipherType := reflect.TypeOf(&bindings.ChannelDbCipher{}) - - if binCipherType.NumMethod() != cipherType.NumMethod() { - t.Errorf("WASM ChannelDbCipher object does not have all methods from "+ - "bindings.\nexpected: %d\nreceived: %d", - binCipherType.NumMethod(), cipherType.NumMethod()) - } - - for i := 0; i < binCipherType.NumMethod(); i++ { - method := binCipherType.Method(i) - - if _, exists := cipherType.MethodByName(method.Name); !exists { - t.Errorf("Method %s does not exist.", method.Name) - } - } -} - type jsIdentity struct { pubKey js.Value codeset js.Value diff --git a/wasm/cipher.go b/wasm/cipher.go new file mode 100644 index 0000000000000000000000000000000000000000..5504d2f3a426c78640ff1611e025103e91fb876e --- /dev/null +++ b/wasm/cipher.go @@ -0,0 +1,232 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 ( + "github.com/pkg/errors" + "gitlab.com/elixxir/client/v4/bindings" + "gitlab.com/elixxir/client/v4/storage/utility" + "gitlab.com/elixxir/crypto/indexedDb" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" + "sync" + "syscall/js" +) + +// dbCipherTrackerSingleton is used to track DbCipher objects +// so that they can be referenced by ID back over the bindings. +var dbCipherTrackerSingleton = &DbCipherTracker{ + tracked: make(map[int]*DbCipher), + count: 0, +} + +// DbCipherTracker is a singleton used to keep track of extant +// DbCipher objects, preventing race conditions created by passing it +// over the bindings. +type DbCipherTracker struct { + tracked map[int]*DbCipher + count int + mux sync.RWMutex +} + +// create creates a DbCipher from a [indexedDb.Cipher], assigns it a unique +// ID, and adds it to the DbCipherTracker. +func (ct *DbCipherTracker) create(c indexedDb.Cipher) *DbCipher { + ct.mux.Lock() + defer ct.mux.Unlock() + + chID := ct.count + ct.count++ + + ct.tracked[chID] = &DbCipher{ + api: c, + id: chID, + } + + return ct.tracked[chID] +} + +// get an DbCipher from the DbCipherTracker given its ID. +func (ct *DbCipherTracker) get(id int) (*DbCipher, error) { + ct.mux.RLock() + defer ct.mux.RUnlock() + + c, exist := ct.tracked[id] + if !exist { + return nil, errors.Errorf( + "Cannot get DbCipher for ID %d, does not exist", id) + } + + return c, nil +} + +// delete removes a DbCipherTracker from the DbCipherTracker. +func (ct *DbCipherTracker) delete(id int) { + ct.mux.Lock() + defer ct.mux.Unlock() + + delete(ct.tracked, id) +} + +// DbCipher wraps the [indexedDb.Cipher] object so its methods +// can be wrapped to be Javascript compatible. +type DbCipher struct { + api indexedDb.Cipher + salt []byte + id int +} + +// newDbCipherJS creates a new Javascript compatible object +// (map[string]any) that matches the [DbCipher] structure. +func newDbCipherJS(c *DbCipher) map[string]any { + DbCipherMap := map[string]any{ + "GetID": js.FuncOf(c.GetID), + "Encrypt": js.FuncOf(c.Encrypt), + "Decrypt": js.FuncOf(c.Decrypt), + "MarshalJSON": js.FuncOf(c.MarshalJSON), + "UnmarshalJSON": js.FuncOf(c.UnmarshalJSON), + } + + return DbCipherMap +} + +// NewDatabaseCipher constructs a [DbCipher] object. +// +// Parameters: +// - args[0] - The tracked [Cmix] object ID (int). +// - args[1] - The password for storage. This should be the same password +// passed into [NewCmix] (Uint8Array). +// - args[2] - The maximum size of a payload to be encrypted. A payload passed +// into [DbCipher.Encrypt] that is larger than this value will result +// in an error (int). +// +// Returns: +// - JavaScript representation of the [DbCipher] object. +// - Throws an error if creating the cipher fails. +func NewDatabaseCipher(_ js.Value, args []js.Value) any { + cmixId := args[0].Int() + password := utils.CopyBytesToGo(args[1]) + plaintTextBlockSize := args[2].Int() + + // Get user from singleton + user, err := bindings.GetCMixInstance(cmixId) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + // Generate RNG + stream := user.GetRng().GetStream() + + // Load or generate a salt + salt, err := utility.NewOrLoadSalt( + user.GetStorage().GetKV(), stream) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + // Construct a cipher + c, err := indexedDb.NewCipher( + password, salt, plaintTextBlockSize, stream) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + // Add to singleton and return + return newDbCipherJS(dbCipherTrackerSingleton.create(c)) +} + +// GetID returns the ID for this [DbCipher] in the +// DbCipherTracker. +// +// Returns: +// - Tracker ID (int). +func (c *DbCipher) GetID(js.Value, []js.Value) any { + return c.id +} + +// Encrypt will encrypt the raw data. It will return a ciphertext. Padding is +// done on the plaintext so all encrypted data looks uniform at rest. +// +// Parameters: +// - args[0] - The data to be encrypted (Uint8Array). This must be smaller +// than the block size passed into [NewDatabaseCipher]. If it is +// larger, this will return an error. +// +// Returns: +// - The ciphertext of the plaintext passed in (String). +// - Throws an error if it fails to encrypt the plaintext. +func (c *DbCipher) Encrypt(_ js.Value, args []js.Value) any { + ciphertext, err := c.api.Encrypt(utils.CopyBytesToGo(args[0])) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + return ciphertext +} + +// Decrypt will decrypt the passed in encrypted value. The plaintext will be +// returned by this function. Any padding will be discarded within this +// function. +// +// Parameters: +// - args[0] - the encrypted data returned by [DbCipher.Encrypt] +// (String). +// +// Returns: +// - The plaintext of the ciphertext passed in (Uint8Array). +// - Throws an error if it fails to encrypt the plaintext. +func (c *DbCipher) Decrypt(_ js.Value, args []js.Value) any { + plaintext, err := c.api.Decrypt(args[0].String()) + if err != nil { + exception.ThrowTrace(err) + return nil + } + + return utils.CopyBytesToJS(plaintext) +} + +// MarshalJSON marshals the cipher into valid JSON. +// +// Returns: +// - JSON of the cipher (Uint8Array). +// - Throws an error if marshalling fails. +func (c *DbCipher) MarshalJSON(js.Value, []js.Value) any { + data, err := c.api.MarshalJSON() + if err != nil { + exception.ThrowTrace(err) + return nil + } + + return utils.CopyBytesToJS(data) +} + +// UnmarshalJSON unmarshalls JSON into the cipher. +// +// Note that this function does not transfer the internal RNG. Use +// [indexedDb.NewCipherFromJSON] to properly reconstruct a cipher from JSON. +// +// Parameters: +// - args[0] - JSON data to unmarshal (Uint8Array). +// +// Returns: +// - JSON of the cipher (Uint8Array). +// - Throws an error if marshalling fails. +func (c *DbCipher) UnmarshalJSON(_ js.Value, args []js.Value) any { + err := c.api.UnmarshalJSON(utils.CopyBytesToGo(args[0])) + if err != nil { + exception.ThrowTrace(err) + return nil + } + return nil +} diff --git a/wasm/cipher_test.go b/wasm/cipher_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0f83461509bc0f70ff5afd7104de9ab5fb3e6e11 --- /dev/null +++ b/wasm/cipher_test.go @@ -0,0 +1,35 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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" +) + +// Tests that the map representing DbCipher returned by +// newDbCipherJS contains all the methods on DbCipher. +func Test_newChannelDbCipherJS(t *testing.T) { + cipherType := reflect.TypeOf(&DbCipher{}) + + cipher := newDbCipherJS(&DbCipher{}) + if len(cipher) != cipherType.NumMethod() { + t.Errorf("DbCipher JS object does not have all methods."+ + "\nexpected: %d\nreceived: %d", cipherType.NumMethod(), len(cipher)) + } + + for i := 0; i < cipherType.NumMethod(); i++ { + method := cipherType.Method(i) + + if _, exists := cipher[method.Name]; !exists { + t.Errorf("Method %s does not exist.", method.Name) + } + } +} diff --git a/wasm/cmix.go b/wasm/cmix.go index e3430f4cf4b489ad7480ab53522fac2631c7cbce..0231e368471d922a9b2b162950d4cf8d6e7290db 100644 --- a/wasm/cmix.go +++ b/wasm/cmix.go @@ -10,11 +10,19 @@ package wasm import ( - "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" + "fmt" + "sync/atomic" "syscall/js" + + "gitlab.com/elixxir/client/v4/bindings" + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" ) +// initializing prevents a synchronized Cmix object from being loaded while one +// is being initialized. +var initializing atomic.Bool + // Cmix wraps the [bindings.Cmix] object so its methods can be wrapped to be // Javascript compatible. type Cmix struct { @@ -29,6 +37,7 @@ func newCmixJS(api *bindings.Cmix) map[string]any { // cmix.go "GetID": js.FuncOf(c.GetID), "GetReceptionID": js.FuncOf(c.GetReceptionID), + "GetRemoteKV": js.FuncOf(c.GetRemoteKV), "EKVGet": js.FuncOf(c.EKVGet), "EKVSet": js.FuncOf(c.EKVSet), @@ -98,7 +107,49 @@ 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() + } + } + + return utils.CreatePromise(promiseFn) +} + +// NewSynchronizedCmix clones a cMix from remote storage. +// +// Users of this function should delete the storage directory on error. +// +// Parameters: +// - args[0] - NDF JSON ([ndf.NetworkDefinition]) (string). +// - args[1] - Storage directory path (string). +// - args[2] - The remote "directory" or path prefix used by the RemoteStore +// when reading/writing files (string). +// - args[3] - Password used for storage (Uint8Array). +// - args[4] - Javascript [RemoteStore] implementation. +// +// Returns a promise: +// - Resolves on success. +// - Rejected with an error if creating a new cMix client fails. +func NewSynchronizedCmix(_ js.Value, args []js.Value) any { + ndfJSON := args[0].String() + storageDir := args[1].String() + remoteStoragePrefixPath := args[2].String() + password := utils.CopyBytesToGo(args[3]) + rs := newRemoteStore(args[4]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + // Block loading of synchronized Cmix during initialisation + initializing.Store(true) + + err := bindings.NewSynchronizedCmix(ndfJSON, storageDir, + remoteStoragePrefixPath, password, rs) + + // Unblock loading of synchronized Cmix during initialisation + initializing.Store(false) + + if err != nil { + reject(exception.NewTrace(err)) } else { resolve() } @@ -131,9 +182,49 @@ func LoadCmix(_ js.Value, args []js.Value) any { cmixParamsJSON := utils.CopyBytesToGo(args[2]) promiseFn := func(resolve, reject func(args ...any) js.Value) { - net, err := bindings.LoadCmix(storageDir, password, cmixParamsJSON) + 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] - The remote "directory" or path prefix used by the RemoteStore +// when reading/writing files (string). +// - args[2] - Password used for storage (Uint8Array). +// - args[3] - Javascript [RemoteStore] implementation. +// - args[4] - 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() + remoteStoragePrefixPath := args[1].String() + password := utils.CopyBytesToGo(args[2]) + rs := newRemoteStore(args[3]) + cmixParamsJSON := utils.CopyBytesToGo(args[4]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + if initializing.Load() { + reject(exception.NewTrace(fmt.Errorf( + "cannot Load when New is running"))) + } + net, err := bindings.LoadSynchronizedCmix(storageDir, + remoteStoragePrefixPath, password, + rs, cmixParamsJSON) + if err != nil { + reject(exception.NewTrace(err)) } else { resolve(newCmixJS(net)) } @@ -158,6 +249,19 @@ 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, []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: @@ -172,7 +276,7 @@ func (c *Cmix) EKVGet(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { val, err := c.api.EKVGet(key) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(val)) } @@ -197,7 +301,7 @@ func (c *Cmix) EKVSet(_ js.Value, args []js.Value) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { err := c.api.EKVSet(key, val) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(nil) } diff --git a/wasm/collective.go b/wasm/collective.go new file mode 100644 index 0000000000000000000000000000000000000000..e7202220100ea61ef47a672bf605b385a588398d --- /dev/null +++ b/wasm/collective.go @@ -0,0 +1,678 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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), + "GetAllRemoteKeyListeners": js.FuncOf( + rkv.GetAllRemoteKeyListeners), + "GetRemoteKeyListeners": js.FuncOf(rkv.GetRemoteKeyListeners), + "DeleteRemoteKeyListener": js.FuncOf( + rkv.DeleteRemoteKeyListener), + "GetAllRemoteMapListeners": js.FuncOf( + rkv.GetAllRemoteMapListeners), + "GetRemoteMapListeners": js.FuncOf(rkv.GetRemoteMapListeners), + "DeleteRemoteMapListener": js.FuncOf( + rkv.DeleteRemoteMapListener), + } + + 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 promise: +// - Resolves to the prefix (string). +func (r *RemoteKV) GetPrefix(js.Value, []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. +// +// Returns a promise: +// - Resolves to a new [RemoteKV]. +// - Rejected with an error on failure. +func (r *RemoteKV) Root(js.Value, []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. +// +// Returns a promise: +// - Resolves to a boolean. +// - Rejected with an error. +func (r *RemoteKV) IsMemStore(js.Value, []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 ID of the callback or -1. +// The version and "localEvents" flags are only respected on first call. +// +// Parameters: +// - args[0] - The key (string). +// - args[1] - The version (int). +// - args[2] - The [KeyChangedByRemoteCallback] Javascript callback. +// - args[3] - Toggle local events (optional) (boolean). +// +// Returns a promise: +// - Resolves to the callback ID (int). +// - Rejects with an error on failure. +func (r *RemoteKV) ListenOnRemoteKey(_ js.Value, args []js.Value) any { + key := args[0].String() + version := int64(args[1].Int()) + cb := newKeyChangedByRemoteCallback(args[2]) + + localEvents := true + if len(args) > 3 && !args[3].IsUndefined() { + localEvents = args[3].Bool() + } + + id, err := r.api.ListenOnRemoteKey(key, version, cb, + localEvents) + if err != nil { + exception.ThrowTrace(err) + } + return id +} + +// ListenOnRemoteMap allows the caller to receive updates when the map or map +// elements are updated. It returns the ID of the callback or -1 and an error. +// The version and "localEvents" flags are only respected on first call. +// +// Parameters: +// - args[0] - The key (string). +// - args[1] - The version (int). +// - args[2] - the [MapChangedByRemoteCallback] javascript callback +// - args[3] - Toggle local events (optional) (boolean). +// +// Returns a promise: +// - Resolves to the callback ID (int). +// - Rejects with an error on failure. +func (r *RemoteKV) ListenOnRemoteMap(_ js.Value, args []js.Value) any { + mapName := args[0].String() + version := int64(args[1].Int()) + cb := newMapChangedByRemoteCallback(args[2]) + + localEvents := true + if len(args) > 3 && !args[3].IsUndefined() { + localEvents = args[3].Bool() + } + + id, err := r.api.ListenOnRemoteMap(mapName, version, cb, + localEvents) + if err != nil { + exception.ThrowTrace(err) + } + return id +} + +// GetAllRemoteKeyListeners returns a JSON list of { key: [id, id, id, ...] }, +// where key is the key for the listener and the list is an list of integer ids +// of each listener. +func (r *RemoteKV) GetAllRemoteKeyListeners(_ js.Value, args []js.Value) any { + return r.api.GetAllRemoteKeyListeners() +} + +// GeRemoteKeyListeners returns a JSON list of [id, id, id, ...], +// where the list is an list of integer ids of each listener. +// +// Parameters: +// - args[0] - the key to look at +func (r *RemoteKV) GetRemoteKeyListeners(_ js.Value, args []js.Value) any { + key := args[0].String() + return r.api.GetRemoteKeyListeners(key) +} + +// DeleteRemoteKeyListener deletes a specific listener for a key. +// +// Parameters: +// - args[0] - the key to delete for +// - args[1] - the id of the listener +func (r *RemoteKV) DeleteRemoteKeyListener(_ js.Value, args []js.Value) any { + key := args[0].String() + id := args[1].Int() + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := r.api.DeleteRemoteKeyListener(key, id) + if err != nil { + reject(exception.NewTrace(err)) + } else { + resolve() + } + } + return utils.CreatePromise(promiseFn) +} + +// GetAllRemoteMapListeners returns a JSON list of { key: [id, id, id, ...] }, +// where key is the key for the listener and the list is an list of integer ids +// of each listener. +func (r *RemoteKV) GetAllRemoteMapListeners(_ js.Value, args []js.Value) any { + return r.api.GetAllRemoteMapListeners() +} + +// GeRemoteMapListeners returns a JSON list of [id, id, id, ...], +// where the list is an list of integer ids of each listener. +// +// Parameters: +// - args[0] - the key to look at +func (r *RemoteKV) GetRemoteMapListeners(_ js.Value, args []js.Value) any { + key := args[0].String() + return r.api.GetRemoteMapListeners(key) +} + +// DeleteRemoteMapListener deletes a specific listener for a key. +// +// Parameters: +// - args[0] - the mapName to delete for +// - args[1] - the id of the listener +func (r *RemoteKV) DeleteRemoteMapListener(_ js.Value, args []js.Value) any { + mapName := args[0].String() + id := args[1].Int() + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := r.api.DeleteRemoteMapListener(mapName, id) + if err != nil { + reject(exception.NewTrace(err)) + } else { + resolve() + } + } + 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) { + v, awaitErr := utils.Await(rsCB.read(path)) + if awaitErr != nil { + return nil, js.Error{Value: awaitErr[0]} + } + return utils.CopyBytesToGo(v[0]), nil +} + +// 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 { + _, awaitErr := utils.Await(rsCB.write(path, utils.CopyBytesToJS(data))) + if awaitErr != nil { + return js.Error{Value: awaitErr[0]} + } + return nil +} + +// 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) (string, error) { + v, err := utils.Await(rsCB.getLastModified(path)) + if err != nil { + return "", js.Error{Value: err[0]} + } + return v[0].String(), nil +} + +// 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() (string, error) { + v, err := utils.Await(rsCB.getLastWrite()) + if err != nil { + return "", js.Error{Value: err[0]} + } + return v[0].String(), nil +} + +// 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) { + v, awaitErr := utils.Await(rsCB.readDir(path)) + if awaitErr != nil { + return nil, js.Error{Value: awaitErr[0]} + } + return utils.CopyBytesToGo(v[0]), nil +} + +//////////////////////////////////////////////////////////////////////////////// +// 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 50aebb08a1ac9bdbce7ba5d7e53c2f0591a24577..249f59e95c8f68b4e0d457d97a560faf3442f6a1 100644 --- a/wasm/dm.go +++ b/wasm/dm.go @@ -10,7 +10,6 @@ package wasm import ( - "crypto/ed25519" "encoding/base64" "encoding/json" "syscall/js" @@ -20,8 +19,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" ) //////////////////////////////////////////////////////////////////////////////// @@ -49,23 +49,49 @@ func newDMClientJS(api *bindings.DMClient) map[string]any { "ExportPrivateIdentity": js.FuncOf(cm.ExportPrivateIdentity), "GetNickname": js.FuncOf(cm.GetNickname), "SetNickname": js.FuncOf(cm.SetNickname), + "BlockPartner": js.FuncOf(cm.BlockPartner), + "UnblockPartner": js.FuncOf(cm.UnblockPartner), "IsBlocked": js.FuncOf(cm.IsBlocked), - "GetBlockedSenders": js.FuncOf(cm.GetBlockedSenders), + "GetBlockedPartners": js.FuncOf(cm.GetBlockedPartners), "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), - "Send": js.FuncOf(cm.Send), + "SendText": js.FuncOf(cm.SendText), + "SendReply": js.FuncOf(cm.SendReply), + "SendReaction": js.FuncOf(cm.SendReaction), + "SendSilent": js.FuncOf(cm.SendSilent), + "SendInvite": js.FuncOf(cm.SendInvite), + "DeleteMessage": js.FuncOf(cm.DeleteMessage), + "Send": js.FuncOf(cm.Send), + + // Notifications + "GetNotificationLevel": js.FuncOf(cm.GetNotificationLevel), + "SetMobileNotificationsLevel": js.FuncOf( + cm.SetMobileNotificationsLevel), } return dmClientMap } +// dmCallbacks wraps Javascript callbacks to adhere to the +// [bindings.DmCallbacks] interface. +type dmCallbacks struct { + eventUpdate func(args ...any) js.Value +} + +// newDmCallbacks adds the callbacks from the Javascript object. +func newDmCallbacks(value js.Value) *dmCallbacks { + return &dmCallbacks{eventUpdate: utils.WrapCB(value, "EventUpdate")} +} + +// EventUpdate implements [bindings.DmCallbacks.EventUpdate]. +func (dmCBS *dmCallbacks) EventUpdate(eventType int64, jsonData []byte) { + dmCBS.eventUpdate(eventType, utils.CopyBytesToJS(jsonData)) +} + // NewDMClient creates a new [DMClient] from a private identity // ([codename.PrivateIdentity]), used for direct messaging. // @@ -77,22 +103,32 @@ func newDMClientJS(api *bindings.DMClient) map[string]any { // Parameters: // - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved // using [Cmix.GetID]. -// - args[1] - Bytes of a private identity ([codename.PrivateIdentity]) that +// - args[1] - ID of [Notifications] object in tracker. This can be retrieved +// using [Notifications.GetID] (int). +// - args[2] - Bytes of a private identity ([codename.PrivateIdentity]) that // is generated by [codename.GenerateIdentity] (Uint8Array). -// - args[2] - A function that initialises and returns a Javascript object +// - 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] - A Javascript object that implements the function on +// [bindings.DmCallbacks]. It is a callback that informs the UI about +// updates relating to DM conversations. The interface may be null, but if +// one is provided, each method must be implemented. // // 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]) + cmixID := args[0].Int() + notificationsID := args[1].Int() + privateIdentity := utils.CopyBytesToGo(args[2]) + em := newDMReceiverBuilder(args[3]) + cbs := newDmCallbacks(args[4]) - cm, err := bindings.NewDMClient(args[0].Int(), privateIdentity, em) + cm, err := + bindings.NewDMClient(cmixID, notificationsID, privateIdentity, em, cbs) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) return nil } @@ -113,36 +149,37 @@ func NewDMClient(_ js.Value, args []js.Value) any { // Parameters: // - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved // using [Cmix.GetID]. -// - args[1] - Path to Javascript file that starts the worker (string). -// - args[2] - Bytes of a private identity ([codename.PrivateIdentity]) that +// - args[1] - ID of [Notifications] object in tracker. This can be retrieved +// using [Notifications.GetID] (int). +// - args[2] - ID of [DbCipher] object in tracker (int). Create this object +// with [NewDatabaseCipher] and get its id with [DbCipher.GetID]. +// - args[3] - Path to Javascript file that starts the worker (string). +// - args[4] - Bytes of a private identity ([codename.PrivateIdentity]) that // is generated by [codename.GenerateIdentity] (Uint8Array). -// - args[3] - The message receive callback. It is a function that takes in -// the same parameters as [dm.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. -// messageUpdate is true if the message already exists and was edited. -// conversationUpdate is true if the Conversation was created or modified. -// - args[4] - ID of [DMDbCipher] object in tracker (int). Create this object -// with [NewDMsDatabaseCipher] and get its id with [DMDbCipher.GetID]. +// - args[5] - A Javascript object that implements the function on +// [bindings.DmCallbacks]. It is a callback that informs the UI about +// updates relating to DM conversations. The interface may be null, but if +// one is provided, each method must be implemented. // // 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() - privateIdentity := utils.CopyBytesToGo(args[2]) - messageReceivedCB := args[3] - cipherID := args[4].Int() + notificationsID := args[1].Int() + cipherID := args[2].Int() + wasmJsPath := args[3].String() + privateIdentity := utils.CopyBytesToGo(args[4]) + cbs := newDmCallbacks(args[5]) - cipher, err := bindings.GetDMDbCipherTrackerFromID(cipherID) + cipher, err := dbCipherTrackerSingleton.get(cipherID) if err != nil { - utils.Throw(utils.TypeError, err) + exception.ThrowTrace(err) } return newDMClientWithIndexedDb( - cmixID, wasmJsPath, privateIdentity, messageReceivedCB, cipher) + cmixID, notificationsID, wasmJsPath, privateIdentity, cipher, cbs) } // NewDMClientWithIndexedDbUnsafe creates a new [DMClient] from a private @@ -160,55 +197,49 @@ func NewDMClientWithIndexedDb(_ js.Value, args []js.Value) any { // Parameters: // - args[0] - ID of [Cmix] object in tracker (int). This can be retrieved // using [Cmix.GetID]. -// - args[1] - Path to Javascript file that starts the worker (string). -// - args[2] - Bytes of a private identity ([codename.PrivateIdentity]) that +// - args[1] - ID of [Notifications] object in tracker. This can be retrieved +// using [Notifications.GetID] (int). +// - args[2] - Path to Javascript file that starts the worker (string). +// - args[3] - Bytes of a private identity ([codename.PrivateIdentity]) that // is generated by [codename.GenerateIdentity] (Uint8Array). -// - args[3] - The message receive callback. It is a function that takes in -// the same parameters as [dm.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. -// messageUpdate is true if the message already exists and was edited. -// conversationUpdate is true if the Conversation was created or modified. +// - args[4] - A Javascript object that implements the function on +// [bindings.DmCallbacks]. It is a callback that informs the UI about +// updates relating to DM conversations. The interface may be null, but if +// one is provided, each method must be implemented. // // Returns a promise: // - Resolves to a Javascript representation of the [DMClient] object. // - Rejected with an error if loading indexedDbWorker or the manager fails. func NewDMClientWithIndexedDbUnsafe(_ js.Value, args []js.Value) any { cmixID := args[0].Int() - wasmJsPath := args[1].String() - privateIdentity := utils.CopyBytesToGo(args[2]) - messageReceivedCB := args[3] + notificationsID := args[1].Int() + wasmJsPath := args[2].String() + privateIdentity := utils.CopyBytesToGo(args[3]) + cbs := newDmCallbacks(args[4]) return newDMClientWithIndexedDb( - cmixID, wasmJsPath, privateIdentity, messageReceivedCB, nil) + cmixID, notificationsID, wasmJsPath, privateIdentity, nil, cbs) } -func newDMClientWithIndexedDb(cmixID int, wasmJsPath string, - privateIdentity []byte, cb js.Value, cipher *bindings.DMDbCipher) any { - - messageReceivedCB := func(uuid uint64, pubKey ed25519.PublicKey, - messageUpdate, conversationUpdate bool) { - cb.Invoke(uuid, utils.CopyBytesToJS(pubKey[:]), - messageUpdate, conversationUpdate) - } +func newDMClientWithIndexedDb(cmixID, notificationsID int, wasmJsPath string, + privateIdentity []byte, cipher *DbCipher, cbs *dmCallbacks) any { promiseFn := func(resolve, reject func(args ...any) js.Value) { - 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) + model, err := + indexDB.NewWASMEventModel(dmPath, wasmJsPath, cipher.api, cbs) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } cm, err := bindings.NewDMClientWithGoEventModel( - cmixID, privateIdentity, model) + cmixID, notificationsID, privateIdentity, model, cbs) if err != nil { - reject(utils.JsTrace(err)) + reject(exception.NewTrace(err)) } else { resolve(newDMClientJS(cm)) } @@ -259,7 +290,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 } @@ -271,11 +302,11 @@ func (dmc *DMClient) ExportPrivateIdentity(_ js.Value, args []js.Value) any { // // Returns: // - The nickname (string). -// - Throws TypeError if the channel has no nickname set. +// - Throws an error if the channel has no nickname set. 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 } @@ -286,38 +317,82 @@ func (dmc *DMClient) GetNickname(_ js.Value, _ []js.Value) any { // // Parameters: // - args[0] - The nickname to set (string). +// +// Returns: +// - Throws an error if setting the nickname fails. func (dmc *DMClient) SetNickname(_ js.Value, args []js.Value) any { - dmc.api.SetNickname(args[0].String()) + err := dmc.api.SetNickname(args[0].String()) + if err != nil { + exception.ThrowTrace(err) + return nil + } + return nil } -// IsBlocked indicates if the given sender is blocked. -// Blocking is controlled by the receiver/EventModel. +// BlockPartner prevents receiving messages and notifications from the partner. // // Parameters: -// - args[0] - Bytes of the sender's ED25519 public key (Uint8Array). +// - args[0] - The partner's [ed25519.PublicKey] key to block (Uint8Array). +// +// Returns a promise that exits upon completion. +func (dmc *DMClient) BlockPartner(_ js.Value, args []js.Value) any { + partnerPubKey := utils.CopyBytesToGo(args[0]) + promiseFn := func(resolve, reject func(args ...any) js.Value) { + dmc.api.BlockPartner(partnerPubKey) + resolve() + } + return utils.CreatePromise(promiseFn) +} + +// UnblockPartner unblocks a blocked partner to allow DM messages. +// +// Parameters: +// - args[0] - The partner's [ed25519.PublicKey] to unblock (Uint8Array). +func (dmc *DMClient) UnblockPartner(_ js.Value, args []js.Value) any { + partnerPubKey := utils.CopyBytesToGo(args[0]) + promiseFn := func(resolve, reject func(args ...any) js.Value) { + dmc.api.UnblockPartner(partnerPubKey) + resolve() + } + return utils.CreatePromise(promiseFn) +} + +// IsBlocked indicates if the given partner is blocked. +// +// Parameters: +// - args[0] - The partner's [ed25519.PublicKey] public key to check +// (Uint8Array). // // Returns: // - boolean func (dmc *DMClient) IsBlocked(_ js.Value, args []js.Value) any { - return dmc.api.IsBlocked(utils.CopyBytesToGo(args[0])) + partnerPubKey := utils.CopyBytesToGo(args[0]) + promiseFn := func(resolve, reject func(args ...any) js.Value) { + isBlocked := dmc.api.IsBlocked(partnerPubKey) + resolve(isBlocked) + } + return utils.CreatePromise(promiseFn) } -// GetBlockedSenders returns the ED25519 public keys of all senders who are -// blocked by this user. Blocking is controlled by the receiver/EventModel. +// GetBlockedPartners returns all partners who are blocked by this user. // // Returns: // - JSON of an array of [ed25519.PublicKey] (Uint8Array) // -// Example JSON return: +// Example return: // // [ -// "v3TON4ju4FxFvp3D/Df0OFV50QSqmiHPQ/BOHMwRRJ8=", -// "ZsehfwnncIx4NC8WZyhbypC3nfGsiqU21T+bPRC+iIU=", -// "ZuBal443tYZ4j025A6q9xU7xn9ZQF5xB1hbh6LxpBAQ=" +// "TYWuCfyGBjNWDtl/Roa6f/o206yYPpuB6sX2kJZTe98=", +// "4JLRzgtW1SZ9c5pE+v0WwrGPj1t19AuU6Gg5IND5ymA=", +// "CWDqF1bnhulW2pko+zgmbDZNaKkmNtFdUgY4bTm2DhA=" // ] -func (dmc *DMClient) GetBlockedSenders(_ js.Value, args []js.Value) any { - return dmc.api.IsBlocked(utils.CopyBytesToGo(args[0])) +func (dmc *DMClient) GetBlockedPartners(js.Value, []js.Value) any { + promiseFn := func(resolve, reject func(args ...any) js.Value) { + blocked := utils.CopyBytesToJS(dmc.api.GetBlockedPartners()) + resolve(blocked) + } + return utils.CreatePromise(promiseFn) } //////////////////////////////////////////////////////////////////////////////// @@ -359,7 +434,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)) } @@ -417,7 +492,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)) } @@ -432,7 +507,8 @@ func (dmc *DMClient) SendReply(_ js.Value, args []js.Value) any { // Users will drop the reaction if they do not recognize the reactTo message. // // Parameters: -// - args[0] - Marshalled bytes of the channel [id.ID] (Uint8Array). +// - 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] - The user's reaction. This should be a single emoji with no // other characters. As such, a Unicode string is expected (string). @@ -441,7 +517,7 @@ func (dmc *DMClient) SendReply(_ js.Value, args []js.Value) any { // your own. Alternatively, if reacting to another user's message, you may // retrieve it via the [bindings.ChannelMessageReceptionCallback] registered // using [ChannelsManager.RegisterReceiveHandler] (Uint8Array). -// - args[3] - JSON of [xxdk.CMIXParams]. If left empty +// - args[4] - JSON of [xxdk.CMIXParams]. If left empty // [bindings.GetDefaultCMixParams] will be used internally (Uint8Array). // // Returns a promise: @@ -464,7 +540,128 @@ 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)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// SendInvite is used to send to a DM partner an invitation to another +// channel. +// +// If the channel ID for the invitee channel is not recognized by the Manager, +// then an error will be returned. +// +// 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 the invitee channel [id.ID]. +// This can be retrieved from [GetChannelJSON]. (Uint8Array). +// - args[3] - The contents of the message. The message should be at most 510 +// bytes. This is expected to be Unicode, and thus a string data type is +// expected. +// - args[4] - The URL to append the channel info to. +// - args[5] - A JSON marshalled [xxdk.CMIXParams]. This may be empty, +// and GetDefaultCMixParams will be used internally. +// +// Returns a promise: +// - Resolves to the JSON of [bindings.ChannelSendReport] (Uint8Array). +// - Rejected with an error if sending fails. +func (dmc *DMClient) SendInvite(_ js.Value, args []js.Value) any { + var ( + partnerPubKeyBytes = utils.CopyBytesToGo(args[0]) + partnerToken = int32(args[1].Int()) + inviteToJSON = utils.CopyBytesToGo(args[2]) + msg = args[3].String() + host = args[4].String() + cmixParamsJSON = utils.CopyBytesToGo(args[5]) + ) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := dmc.api.SendInvite( + partnerPubKeyBytes, partnerToken, inviteToJSON, msg, host, + cmixParamsJSON) + if err != nil { + reject(exception.NewTrace(err)) + } else { + resolve(utils.CopyBytesToJS(sendReport)) + } + } + + return utils.CreatePromise(promiseFn) +} + +// DeleteMessage sends a message to the partner to delete a message this user +// sent. Also deletes it from the local database. +// +// 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] -The bytes of the [message.ID] of the message to delete. This may +// be found in the [bindings.ChannelSendReport] (Uint8Array). +// - args[3] - 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) DeleteMessage(_ js.Value, args []js.Value) any { + partnerPubKeyBytes := utils.CopyBytesToGo(args[0]) + partnerToken := int32(args[1].Int()) + targetMessageIdBytes := utils.CopyBytesToGo(args[2]) + cmixParamsJSON := utils.CopyBytesToGo(args[3]) + + jww.DEBUG.Printf("DeleteMessage(%s, %d, %s)", + base64.RawStdEncoding.EncodeToString(partnerPubKeyBytes)[:8], + partnerToken, + base64.RawStdEncoding.EncodeToString(targetMessageIdBytes)) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + sendReport, err := dmc.api.DeleteMessage(partnerPubKeyBytes, + partnerToken, targetMessageIdBytes, cmixParamsJSON) + if err != nil { + reject(exception.NewTrace(err)) } else { resolve(utils.CopyBytesToJS(sendReport)) } @@ -515,7 +712,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)) } @@ -566,7 +763,7 @@ type DMUser struct { PublicKey []byte `json:"publicKey"` } -// GetShareURL generates a URL that can be used to share a URL to initiate a +// GetShareURL generates a URL that can be used to share a URL to initiate d // direct messages with this user. // // Parameters: @@ -574,17 +771,72 @@ type DMUser struct { // // Returns: // - JSON of [DMShareURL] (Uint8Array). +// - Throws an exception on error. 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 } return utils.CopyBytesToJS(urlReport) } +// GetNotificationLevel gets the notification level for a given DM partner's +// public key +// +// Parameters: +// - args[0] - The partner's [ed25519.PublicKey] (Uint8Array) +// +// Returns: +// - The [dm.NotificationLevel] of the DM conversation (int). +// +// Returns a promise: +// - Resolves to the [dm.NotificationLevel] of the DM conversation (int). +// - Rejected with an error if getting the notification level fails. +func (dmc *DMClient) GetNotificationLevel(_ js.Value, args []js.Value) any { + partnerPubKey := utils.CopyBytesToGo(args[0]) + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + level, err := dmc.api.GetNotificationLevel(partnerPubKey) + if err != nil { + reject(exception.NewTrace(err)) + } else { + resolve(level) + } + } + + return utils.CreatePromise(promiseFn) +} + +// SetMobileNotificationsLevel sets the notification level for the given DM +// conversation partner. +// +// Parameters: +// - args[0] - The partner's [ed25519.PublicKey] (Uint8Array). +// - args[1] - The [dm.NotificationLevel] to set for the DM conversation (int). +// +// Returns a promise: +// - Resolves on success. +// - Rejected with an error if setting the notification level fails. +func (dmc *DMClient) SetMobileNotificationsLevel(_ js.Value, + args []js.Value) any { + partnerPubKey := utils.CopyBytesToGo(args[0]) + level := args[1].Int() + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := dmc.api.SetMobileNotificationsLevel(partnerPubKey, level) + if err != nil { + reject(exception.NewTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + // DecodeDMShareURL decodes the user's URL into a [DMUser]. // // Parameters: @@ -593,45 +845,69 @@ func (dmc *DMClient) GetShareURL(_ js.Value, args []js.Value) any { // // Returns: // - JSON of [DMUser] (Uint8Array). +// - Throws an exception on error. 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 } return utils.CopyBytesToJS(report) } -//////////////////////////////////////////////////////////////////////////////// -// Channel Receiving Logic and Callback Registration // -//////////////////////////////////////////////////////////////////////////////// - -// channelMessageReceptionCallback wraps Javascript callbacks to adhere to the -// [bindings.ChannelMessageReceptionCallback] interface. -type dmReceptionCallback struct { - callback func(args ...any) js.Value -} - -// Callback returns the context for a channel message. +// GetDmNotificationReportsForMe 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: -// - receivedChannelMessageReport - Returns the JSON of -// [bindings.ReceivedChannelMessageReport] (Uint8Array). -// - err - Returns an error on failure (Error). +// - args[0] - JSON of [dm.NotificationFilter] (Uint8Array). +// - args[1] - CSV containing notification data (string). // -// Returns: -// - It must return a unique UUID for the message that it can be referenced by -// later (int). -func (cmrCB *dmReceptionCallback) Callback( - receivedChannelMessageReport []byte, err error) int { - uuid := cmrCB.callback( - utils.CopyBytesToJS(receivedChannelMessageReport), - utils.JsTrace(err)) - - return uuid.Int() +// Example JSON of a slice of [dm.NotificationFilter]: +// +// { +// "identifier": "MWL6mvtZ9UUm7jP3ainyI4erbRl+wyVaO5MOWboP0rA=", +// "myID": "AqDqg6Tcs359dBNRBCX7XHaotRDhz1ZRQNXIsGaubvID", +// "tags": [ +// "61334HtH85DPIifvrM+JzRmLqfV5R4AMEmcPelTmFX0=", +// "zc/EPwtx5OKTVdwLcI15bghjJ7suNhu59PcarXE+m9o=", +// "FvArzVJ/082UEpMDCWJsopCLeLnxJV6NXINNkJTk3k8=" +// ], +// "PublicKeys": { +// "61334HtH85DPIifvrM+JzRmLqfV5R4AMEmcPelTmFX0=": "b3HygDv8gjteune9wgBm3YtVuAo2foOusRmj0m5nl6E=", +// "FvArzVJ/082UEpMDCWJsopCLeLnxJV6NXINNkJTk3k8=": "uOLitBZcCh2TEW406jXHJ+Rsi6LybsH8R1u4Mxv/7hA=", +// "zc/EPwtx5OKTVdwLcI15bghjJ7suNhu59PcarXE+m9o=": "lqLD1EzZBxB8PbILUJIfFq4JI0RKThpUQuNlTNgZAWk=" +// }, +// "allowedTypes": {"1": {}, "2": {}} +// } +// +// Returns a promise: +// - Resolves to a JSON of a slice of [dm.NotificationReport] (Uint8Array). +// - Rejected with an error if getting the reports fails. +// +// Example slice of [dm.NotificationReport] return: +// +// [ +// {"partner": "WUSO3trAYeBf4UeJ5TEL+Q4usoyFf0shda0YUmZ3z8k=", "type": 1}, +// {"partner": "5MY652JsVv5YLE6wGRHIFZBMvLklACnT5UtHxmEOJ4o=", "type": 2} +// ] +func GetDmNotificationReportsForMe(_ js.Value, args []js.Value) any { + notificationFilterJson := utils.CopyBytesToGo(args[0]) + notificationDataCsv := args[1].String() + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + forMe, err := bindings.GetDmNotificationReportsForMe( + notificationFilterJson, notificationDataCsv) + if err != nil { + reject(exception.NewTrace(err)) + } else { + resolve(utils.CopyBytesToJS(forMe)) + } + } + + return utils.CreatePromise(promiseFn) } //////////////////////////////////////////////////////////////////////////////// @@ -660,6 +936,9 @@ func (emb *dmReceiverBuilder) Build(path string) bindings.DMReceiver { receiveReply: utils.WrapCB(emJs, "ReceiveReply"), receiveReaction: utils.WrapCB(emJs, "ReceiveReaction"), updateSentStatus: utils.WrapCB(emJs, "UpdateSentStatus"), + deleteMessage: utils.WrapCB(emJs, "DeleteMessage"), + getConversation: utils.WrapCB(emJs, "GetConversation"), + getConversations: utils.WrapCB(emJs, "GetConversations"), } } @@ -671,31 +950,34 @@ type dmReceiver struct { receiveReply func(args ...any) js.Value receiveReaction func(args ...any) js.Value updateSentStatus func(args ...any) js.Value - blockSender func(args ...any) js.Value - unblockSender func(args ...any) js.Value + deleteMessage func(args ...any) js.Value getConversation func(args ...any) js.Value getConversations func(args ...any) js.Value } -// Receive is called whenever a message is received on a given channel. +// Receive is called when a raw direct message is received with unknown type. // It may be called multiple times on the same message. It is incumbent on the // user of the API to filter such called by message ID. // +// The user must interpret the message type and perform their own message +// parsing. +// // Parameters: -// - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array). -// - messageID - The bytes of the [channel.MessageID] of the received message +// - messageID - The bytes of the [dm.MessageID] of the received message // (Uint8Array). // - nickname - The nickname of the sender of the message (string). -// - text - The content of the message (string). -// - pubKey - The sender's Ed25519 public key (Uint8Array). -// - dmToken - The dmToken (int32). -// - codeset - The codeset version (int). +// - text - The bytes content of the message (Uint8Array). +// - partnerKey - The partner's [ed25519.PublicKey]. This is required to +// respond (Uint8Array). +// - senderKey - The sender's [ed25519.PublicKey] (Uint8Array). +// - dmToken - The senders direct messaging token. This is required to respond +// (int). +// - codeset - The codeset version (int) // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). -// - lease - The number of nanoseconds that the message is valid for (int). -// - roundId - The ID of the round that the message was received on (int). -// - msgType - The type of message ([channels.MessageType]) to send (int). -// - status - The [channels.SentStatus] of the message (int). +// - roundID - The ID of the round that the message was received on (int). +// - mType - The type of message ([channels.MessageType]) to send (int). +// - status - the [dm.SentStatus] of the message (int). // // Statuses will be enumerated as such: // @@ -706,39 +988,42 @@ type dmReceiver struct { // Returns: // - A non-negative unique UUID for the message that it can be referenced by // later with [dmReceiver.UpdateSentStatus]. -func (em *dmReceiver) Receive(messageID []byte, nickname string, - text []byte, partnerKey, senderKey []byte, dmToken int32, codeset int, timestamp, +func (em *dmReceiver) Receive(messageID []byte, nickname string, text, + partnerKey, senderKey []byte, dmToken int32, codeset int, timestamp, roundId, mType, status int64) int64 { - uuid := em.receive(messageID, nickname, text, partnerKey, senderKey, dmToken, - codeset, timestamp, roundId, mType, status) + uuid := em.receive(utils.CopyBytesToJS(messageID), nickname, + utils.CopyBytesToJS(text), utils.CopyBytesToJS(partnerKey), + utils.CopyBytesToJS(senderKey), + dmToken, codeset, timestamp, roundId, mType, status) return int64(uuid.Int()) } -// ReceiveText is called whenever a message is received that is a reply on a -// given channel. It may be called multiple times on the same message. It is -// incumbent on the user of the API to filter such called by message ID. +// ReceiveText is called whenever a direct message is received that is a text +// type. It may be called multiple times on the same message. It is incumbent on +// the user of the API to filter such called by message ID. // // Messages may arrive our of order, so a reply in theory can arrive before the // initial message. As a result, it may be important to buffer replies. // // Parameters: -// - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array). -// - messageID - The bytes of the [channel.MessageID] of the received message +// - messageID - The bytes of the [dm.MessageID] of the received message // (Uint8Array). -// - reactionTo - The [channel.MessageID] for the message that received a -// reply (Uint8Array). -// - senderUsername - The username of the sender of the message (string). +// - nickname - The nickname of the sender of the message (string). // - text - The content of the message (string). -// - partnerKey, senderKey - The sender's Ed25519 public key (Uint8Array). -// - dmToken - The dmToken (int32). -// - codeset - The codeset version (int). +// - partnerKey - The partner's [ed25519.PublicKey]. This is required to +// respond (Uint8Array). +// - senderKey - The sender's [ed25519.PublicKey] (Uint8Array). +// - dmToken - The senders direct messaging token. This is required to respond +// (int). +// - codeset - The codeset version (int) // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). -// - lease - The number of nanoseconds that the message is valid for (int). // - roundId - The ID of the round that the message was received on (int). // - msgType - The type of message ([channels.MessageType]) to send (int). // - status - The [channels.SentStatus] of the message (int). +// - roundID - The ID of the round that the message was received on (int). +// - status - the [dm.SentStatus] of the message (int). // // Statuses will be enumerated as such: // @@ -753,36 +1038,40 @@ func (em *dmReceiver) ReceiveText(messageID []byte, nickname, text string, partnerKey, senderKey []byte, dmToken int32, codeset int, timestamp, roundId, status int64) int64 { - uuid := em.receiveText(messageID, nickname, text, partnerKey, senderKey, dmToken, - codeset, timestamp, roundId, status) + uuid := em.receiveText(utils.CopyBytesToJS(messageID), nickname, text, + utils.CopyBytesToJS(partnerKey), utils.CopyBytesToJS(senderKey), + dmToken, codeset, timestamp, roundId, status) return int64(uuid.Int()) } -// ReceiveReply is called whenever a message is received that is a reply on a -// given channel. It may be called multiple times on the same message. It is -// incumbent on the user of the API to filter such called by message ID. +// ReceiveReply is called whenever a direct message is received that is a reply. +// It may be called multiple times on the same message. It is incumbent on the +// user of the API to filter such called by message ID. // // Messages may arrive our of order, so a reply in theory can arrive before the // initial message. As a result, it may be important to buffer replies. // // Parameters: -// - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array). -// - messageID - The bytes of the [channel.MessageID] of the received message +// - messageID - The bytes of the [dm.MessageID] of the received message // (Uint8Array). -// - reactionTo - The [channel.MessageID] for the message that received a -// reply (Uint8Array). -// - senderUsername - The username of the sender of the message (string). +// - reactionTo - The [dm.MessageID] for the message that received a reply +// (Uint8Array). +// - nickname - The nickname of the sender of the message (string). // - text - The content of the message (string). -// - partnerKey, senderKey - The sender's Ed25519 public key (Uint8Array). -// - dmToken - The dmToken (int32). -// - codeset - The codeset version (int). +// - partnerKey - The partner's [ed25519.PublicKey]. This is required to +// respond (Uint8Array). +// - senderKey - The sender's [ed25519.PublicKey] (Uint8Array). +// - dmToken - The senders direct messaging token. This is required to respond +// (int). +// - codeset - The codeset version (int) // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). -// - lease - The number of nanoseconds that the message is valid for (int). // - roundId - The ID of the round that the message was received on (int). // - msgType - The type of message ([channels.MessageType]) to send (int). // - status - The [channels.SentStatus] of the message (int). +// - roundID - The ID of the round that the message was received on (int). +// - status - the [dm.SentStatus] of the message (int). // // Statuses will be enumerated as such: // @@ -793,39 +1082,44 @@ func (em *dmReceiver) ReceiveText(messageID []byte, nickname, text string, // Returns: // - A non-negative unique UUID for the message that it can be referenced by // later with [dmReceiver.UpdateSentStatus]. -func (em *dmReceiver) ReceiveReply(messageID, replyTo []byte, nickname, +func (em *dmReceiver) ReceiveReply(messageID, reactionTo []byte, nickname, text string, partnerKey, senderKey []byte, dmToken int32, codeset int, timestamp, roundId, status int64) int64 { - uuid := em.receiveReply(messageID, replyTo, nickname, text, partnerKey, senderKey, + uuid := em.receiveReply(utils.CopyBytesToJS(messageID), + utils.CopyBytesToJS(reactionTo), nickname, text, + utils.CopyBytesToJS(partnerKey), utils.CopyBytesToJS(senderKey), dmToken, codeset, timestamp, roundId, status) return int64(uuid.Int()) } -// ReceiveReaction is called whenever a reaction to a message is received on a -// given channel. It may be called multiple times on the same reaction. It is +// ReceiveReaction is called whenever a reaction to a direct message is +// received. It may be called multiple times on the same reaction. It is // incumbent on the user of the API to filter such called by message ID. // // Messages may arrive our of order, so a reply in theory can arrive before the // initial message. As a result, it may be important to buffer reactions. // // Parameters: -// - channelID - Marshalled bytes of the channel [id.ID] (Uint8Array). -// - messageID - The bytes of the [channel.MessageID] of the received message +// - messageID - The bytes of the [dm.MessageID] of the received message // (Uint8Array). -// - reactionTo - The [channel.MessageID] for the message that received a -// reply (Uint8Array). -// - senderUsername - The username of the sender of the message (string). -// - reaction - The contents of the reaction message (string). -// - partnerKey, senderKey - The sender's Ed25519 public key (Uint8Array). -// - dmToken - The dmToken (int32). -// - codeset - The codeset version (int). +// - reactionTo - The [dm.MessageID] for the message that received a reply +// (Uint8Array). +// - nickname - The nickname of the sender of the message (string). +// - reaction - The content of the reaction message (string). +// - partnerKey - The partner's [ed25519.PublicKey]. This is required to +// respond (Uint8Array). +// - senderKey - The sender's [ed25519.PublicKey] (Uint8Array). +// - dmToken - The senders direct messaging token. This is required to respond +// (int). +// - codeset - The codeset version (int) // - timestamp - Time the message was received; represented as nanoseconds // since unix epoch (int). -// - lease - The number of nanoseconds that the message is valid for (int). // - roundId - The ID of the round that the message was received on (int). // - msgType - The type of message ([channels.MessageType]) to send (int). // - status - The [channels.SentStatus] of the message (int). +// - roundID - The ID of the round that the message was received on (int). +// - status - the [dm.SentStatus] of the message (int). // // Statuses will be enumerated as such: // @@ -838,16 +1132,16 @@ func (em *dmReceiver) ReceiveReply(messageID, replyTo []byte, nickname, // later with [dmReceiver.UpdateSentStatus]. func (em *dmReceiver) ReceiveReaction(messageID, reactionTo []byte, nickname, reaction string, partnerKey, senderKey []byte, dmToken int32, - codeset int, timestamp, roundId, - status int64) int64 { - uuid := em.receiveReaction(messageID, reactionTo, nickname, reaction, - partnerKey, senderKey, dmToken, codeset, timestamp, roundId, status) + codeset int, timestamp, roundId, status int64) int64 { + uuid := em.receiveReaction(utils.CopyBytesToJS(messageID), + utils.CopyBytesToJS(reactionTo), nickname, reaction, + utils.CopyBytesToJS(partnerKey), utils.CopyBytesToJS(senderKey), + dmToken, codeset, timestamp, roundId, status) return int64(uuid.Int()) } -// UpdateSentStatus is called whenever the sent status of a message has -// changed. +// UpdateSentStatus is called whenever the sent status of a message has changed. // // Parameters: // - uuid - The unique identifier for the message (int). @@ -863,39 +1157,37 @@ func (em *dmReceiver) ReceiveReaction(messageID, reactionTo []byte, // Sent = 0 // Delivered = 1 // Failed = 2 -func (em *dmReceiver) UpdateSentStatus(uuid int64, messageID []byte, - timestamp, roundID, status int64) { - em.updateSentStatus(uuid, utils.CopyBytesToJS(messageID), - timestamp, roundID, status) -} - -// BlockSender silences messages sent by the indicated sender -// public key. -// -// Parameters: -// - senderPubKey - The unique public key for the conversation. -func (em *dmReceiver) BlockSender(senderPubKey []byte) { - em.blockSender(senderPubKey) +func (em *dmReceiver) UpdateSentStatus( + uuid int64, messageID []byte, timestamp, roundID, status int64) { + em.updateSentStatus( + uuid, utils.CopyBytesToJS(messageID), timestamp, roundID, status) } -// UnblockSender silences messages sent by the indicated sender -// public key. +// DeleteMessage deletes the message with the given [message.ID] belonging to +// the sender. If the message exists and belongs to the sender, then it is +// deleted and [DeleteMessage] returns true. If it does not exist, it returns +// false. // // Parameters: -// - senderPubKey - The unique public key for the conversation. -func (em *dmReceiver) UnblockSender(senderPubKey []byte) { - em.unblockSender(senderPubKey) +// - messageID - The bytes of the [message.ID] of the message to delete +// (Uint8Array). +// - senderPubKey - The [ed25519.PublicKey] of the sender of the message +// (Uint8Array). +func (em *dmReceiver) DeleteMessage(messageID, senderPubKey []byte) bool { + return em.deleteMessage( + utils.CopyBytesToJS(messageID), utils.CopyBytesToJS(senderPubKey)).Bool() } // GetConversation returns the conversation held by the model (receiver). // // Parameters: -// - senderPubKey - The unique public key for the conversation. +// - senderPubKey - The unique public key for the conversation (Uint8Array). // // Returns: // - JSON of [dm.ModelConversation] (Uint8Array). func (em *dmReceiver) GetConversation(senderPubKey []byte) []byte { - result := utils.CopyBytesToGo(em.getConversation(senderPubKey)) + result := utils.CopyBytesToGo( + em.getConversation(utils.CopyBytesToJS(senderPubKey))) var conversation dm.ModelConversation err := json.Unmarshal(result, &conversation) @@ -924,146 +1216,6 @@ func (em *dmReceiver) GetConversations() []byte { return conversationsBytes } -//////////////////////////////////////////////////////////////////////////////// -// DM DB Cipher // -//////////////////////////////////////////////////////////////////////////////// - -// DMDbCipher wraps the [bindings.DMDbCipher] object so its methods -// can be wrapped to be Javascript compatible. -type DMDbCipher struct { - api *bindings.DMDbCipher -} - -// newDMDbCipherJS creates a new Javascript compatible object -// (map[string]any) that matches the [DMDbCipher] structure. -func newDMDbCipherJS(api *bindings.DMDbCipher) map[string]any { - c := DMDbCipher{api} - channelDbCipherMap := map[string]any{ - "GetID": js.FuncOf(c.GetID), - "Encrypt": js.FuncOf(c.Encrypt), - "Decrypt": js.FuncOf(c.Decrypt), - "MarshalJSON": js.FuncOf(c.MarshalJSON), - "UnmarshalJSON": js.FuncOf(c.UnmarshalJSON), - } - - return channelDbCipherMap -} - -// NewDMsDatabaseCipher constructs a [DMDbCipher] object. -// -// Parameters: -// - args[0] - The tracked [Cmix] object ID (int). -// - args[1] - The password for storage. This should be the same password -// passed into [NewCmix] (Uint8Array). -// - args[2] - The maximum size of a payload to be encrypted. A payload passed -// into [DMDbCipher.Encrypt] that is larger than this value will result -// in an error (int). -// -// Returns: -// - JavaScript representation of the [DMDbCipher] object. -// - Throws a TypeError if creating the cipher fails. -func NewDMsDatabaseCipher(_ js.Value, args []js.Value) any { - cmixId := args[0].Int() - password := utils.CopyBytesToGo(args[1]) - plaintTextBlockSize := args[2].Int() - - cipher, err := bindings.NewDMsDatabaseCipher( - cmixId, password, plaintTextBlockSize) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil - } - - return newDMDbCipherJS(cipher) -} - -// GetID returns the ID for this [bindings.DMDbCipher] in the -// channelDbCipherTracker. -// -// Returns: -// - Tracker ID (int). -func (c *DMDbCipher) GetID(js.Value, []js.Value) any { - return c.api.GetID() -} - -// Encrypt will encrypt the raw data. It will return a ciphertext. Padding is -// done on the plaintext so all encrypted data looks uniform at rest. -// -// Parameters: -// - args[0] - The data to be encrypted (Uint8Array). This must be smaller -// than the block size passed into [NewDMsDatabaseCipher]. If it is -// larger, this will return an error. -// -// Returns: -// - The ciphertext of the plaintext passed in (Uint8Array). -// - Throws a TypeError 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) - return nil - } - - return utils.CopyBytesToJS(ciphertext) -} - -// Decrypt will decrypt the passed in encrypted value. The plaintext will be -// returned by this function. Any padding will be discarded within this -// function. -// -// Parameters: -// - args[0] - the encrypted data returned by [DMDbCipher.Encrypt] -// (Uint8Array). -// -// Returns: -// - The plaintext of the ciphertext passed in (Uint8Array). -// - Throws a TypeError 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) - return nil - } - - return utils.CopyBytesToJS(plaintext) -} - -// MarshalJSON marshals the cipher into valid JSON. -// -// Returns: -// - JSON of the cipher (Uint8Array). -// - Throws a TypeError 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) - return nil - } - - return utils.CopyBytesToJS(data) -} - -// UnmarshalJSON unmarshalls JSON into the cipher. This function adheres to the -// json.Unmarshaler interface. -// -// Note that this function does not transfer the internal RNG. Use -// [channel.NewCipherFromJSON] to properly reconstruct a cipher from JSON. -// -// Parameters: -// - args[0] - JSON data to unmarshal (Uint8Array). -// -// Returns: -// - JSON of the cipher (Uint8Array). -// - Throws a TypeError 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) - return nil - } - return nil -} - // truncate truncates the string to length n. If the string is trimmed, then // ellipses (...) are appended. func truncate(s string, n int) string { diff --git a/wasm/dm_test.go b/wasm/dm_test.go index 58f837ea0bb857deaede29fd9f5c37f4cd936e34..f959c9463fe793d7cac78927dcdc8eba5980d5f7 100644 --- a/wasm/dm_test.go +++ b/wasm/dm_test.go @@ -62,43 +62,3 @@ func Test_DMClientMethods(t *testing.T) { } } } - -// Tests that the map representing DMDbCipher returned by newDMDbCipherJS -// contains all of the methods on DMDbCipher. -func Test_newDMDbCipherJS(t *testing.T) { - cipherType := reflect.TypeOf(&DMDbCipher{}) - - cipher := newDMDbCipherJS(&bindings.DMDbCipher{}) - if len(cipher) != cipherType.NumMethod() { - t.Errorf("DMDbCipher JS object does not have all methods."+ - "\nexpected: %d\nreceived: %d", cipherType.NumMethod(), len(cipher)) - } - - for i := 0; i < cipherType.NumMethod(); i++ { - method := cipherType.Method(i) - - if _, exists := cipher[method.Name]; !exists { - t.Errorf("Method %s does not exist.", method.Name) - } - } -} - -// Tests that DMDbCipher has all the methods that [bindings.DMDbCipher] has. -func Test_DMDbCipherMethods(t *testing.T) { - cipherType := reflect.TypeOf(&DMDbCipher{}) - binCipherType := reflect.TypeOf(&bindings.DMDbCipher{}) - - if binCipherType.NumMethod() != cipherType.NumMethod() { - t.Errorf("WASM DMDbCipher object does not have all methods from "+ - "bindings.\nexpected: %d\nreceived: %d", - binCipherType.NumMethod(), cipherType.NumMethod()) - } - - for i := 0; i < binCipherType.NumMethod(); i++ { - method := binCipherType.Method(i) - - if _, exists := cipherType.MethodByName(method.Name); !exists { - t.Errorf("Method %s does not exist.", method.Name) - } - } -} 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 dbff9722411ddac19999b28b137739ec31279a45..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 } @@ -165,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 } @@ -186,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 } @@ -205,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 } @@ -225,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 } @@ -271,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 } @@ -377,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 @@ -389,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/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 59c4485c516894972b0cbcfa74b49218f28299f8..3185814f71c14827254fe5cb8f852659c57ed471 100644 --- a/wasm_test.go +++ b/wasm_test.go @@ -39,10 +39,9 @@ func TestPublicFunctions(t *testing.T) { // These functions are used internally by the WASM bindings but are not // exposed - "NewEventModel": {}, "NewChannelsManagerGoEventModel": {}, "LoadChannelsManagerGoEventModel": {}, - "GetChannelDbCipherTrackerFromID": {}, + "GetDbCipherTrackerFromID": {}, // Version functions were renamed to differentiate between WASM and // client versions @@ -50,8 +49,7 @@ func TestPublicFunctions(t *testing.T) { "GetDependencies": {}, // DM Functions these are used but not exported by - // WASM bindins, so are not exposed. - "NewDMReceiver": {}, + // WASM bindings, so are not exposed. "NewDMClientWithGoEventModel": {}, "GetDMDbCipherTrackerFromID": {}, @@ -66,6 +64,9 @@ func TestPublicFunctions(t *testing.T) { // 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..7d5a4c3b5e3ceb5e1784dc5b119d4ea82213ad15 100644 --- a/worker/README.md +++ b/worker/README.md @@ -18,12 +18,12 @@ package main import ( "fmt" - "gitlab.com/elixxir/xxdk-wasm/utils/worker" + "gitlab.com/elixxir/xxdk-wasm/worker" ) func main() { fmt.Println("Starting WebAssembly Worker.") - tm := worker.NewThreadManager("exampleWebWorker") + tm := worker.NewThreadManager("exampleWebWorker", true) tm.SignalReady() <-make(chan bool) } @@ -47,8 +47,8 @@ To start the worker, call `worker.NewManager` with the Javascript file to launch the worker. ```go -wm, err := worker.NewManager("workerWasm.js", "exampleWebWorker") +m, err := worker.NewManager("workerWasm.js", "exampleWebWorker", true) if err != nil { - return nil, err +return nil, err } ``` \ No newline at end of file diff --git a/worker/manager.go b/worker/manager.go index 3a5831db7c07a54927429b8d818dd66db7b9fc7a..ca371a576e67f24e648c70d43b7fe1bffc6ad8c2 100644 --- a/worker/manager.go +++ b/worker/manager.go @@ -10,15 +10,11 @@ package worker import ( - "encoding/json" - "sync" "syscall/js" "time" + "github.com/hack-pad/safejs" "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" - - "gitlab.com/elixxir/xxdk-wasm/utils" ) // initID is the ID for the first item in the callback list. If the list only @@ -31,81 +27,43 @@ const ( // workerInitialConnectionTimeout is the time to wait to receive initial // contact from a new worker before timing out. workerInitialConnectionTimeout = 90 * time.Second - - // ResponseTimeout is the general time to wait after sending a message to - // receive a response before timing out. - ResponseTimeout = 30 * time.Second ) -// receiveQueueChanSize is the size of the channel that received messages are -// put on. -const receiveQueueChanSize = 100 - -// 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. type Manager struct { - // worker is the Worker Javascript object. - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker - worker js.Value - - // callbacks are a list of ReceptionCallback that handle a specific message - // received from the worker. Each callback is keyed on a tag specifying how - // the received message should be handled. If the message is a reply to a - // message sent to the worker, then the callback is also keyed on a unique - // ID. If the message is not a reply, then it appears on initID. - callbacks map[Tag]map[uint64]ReceptionCallback - - // responseIDs is a list of the newest ID to assign to each callback when - // registered. The IDs are used to connect a reply from the worker to the - // original message sent by the main thread. - responseIDs map[Tag]uint64 - - // receiveQueue is the channel that all received messages 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{} - - // name describes the worker. It is used for debugging and logging purposes. - name string + mm *MessageManager - // messageLogging determines if debug message logs should be printed every - // time a message is sent/received to/from the worker. - messageLogging bool - - mux sync.Mutex + // Wrapper of the Worker Javascript object. + // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker + w Worker } // NewManager generates a new Manager. This functions will only return once // communication with the worker has been established. func NewManager(aURL, name string, messageLogging bool) (*Manager, error) { - // Create new worker options with the given name - opts := newWorkerOptions("", "", name) - - m := &Manager{ - worker: js.Global().Get("Worker").New(aURL, opts), - callbacks: make(map[Tag]map[uint64]ReceptionCallback), - responseIDs: make(map[Tag]uint64), - receiveQueue: make(chan js.Value, receiveQueueChanSize), - quit: make(chan struct{}), - name: name, - messageLogging: messageLogging, + w, err := NewWorker(aURL, newWorkerOptions("", "", name)) + if err != nil { + return nil, errors.Wrapf(err, "failed to construct Worker") } - // Start thread to process responses from worker - go m.processThread() + p := DefaultParams() + p.MessageLogging = messageLogging + mm, err := NewMessageManager(w.Value, name+"-main", p) + if err != nil { + return nil, errors.Wrapf(err, "failed to construct message manager") + } - // Register listeners on the Javascript worker object that receive messages - // and errors from the worker - m.addEventListeners() + m := &Manager{ + mm: mm, + w: w, + } // Register a callback that will receive initial message from worker // indicating that it is ready ready := make(chan struct{}) - m.RegisterCallback(readyTag, func([]byte) { ready <- struct{}{} }) + mm.RegisterCallback(readyTag, func([]byte, func([]byte)) { + ready <- struct{}{} + }) // Wait for the ready signal from the worker select { @@ -113,263 +71,124 @@ func NewManager(aURL, name string, messageLogging bool) (*Manager, error) { case <-time.After(workerInitialConnectionTimeout): return nil, errors.Errorf("[WW] [%s] timed out after %s waiting for "+ "initial message from worker", - m.name, workerInitialConnectionTimeout) + mm.name, workerInitialConnectionTimeout) } return m, nil } -// Stop closes the worker manager and terminates the worker. -func (m *Manager) Stop() { - // Stop processThread - select { - case m.quit <- struct{}{}: - } - - // Terminate the worker - go m.terminate() -} - -// processThread processes received messages sequentially. -func (m *Manager) processThread() { - jww.INFO.Printf("[WW] [%s] Starting process thread.", m.name) - for { - select { - case <-m.quit: - jww.INFO.Printf("[WW] [%s] Quitting process thread.", m.name) - return - 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)) - } - } - } -} - -// SendMessage sends a message to the worker with the given tag. If a reception -// callback is specified, then the message is given a unique ID to handle the -// reply. Set receptionCB to nil if no reply is expected. -func (m *Manager) SendMessage( - tag Tag, data []byte, receptionCB ReceptionCallback) { - var id uint64 - if receptionCB != nil { - id = m.registerReplyCallback(tag, receptionCB) - } - - if m.messageLogging { - jww.DEBUG.Printf("[WW] [%s] Main sending message for %q and ID %d "+ - "with data: %s", m.name, tag, id, data) - } +// NewManagerFromScript generates a new Manager. This functions will only return +// once communication with the worker has been established. +// TODO: test or remove +func NewManagerFromScript( + jsScript, name string, messageLogging bool) (*Manager, error) { - msg := Message{ - Tag: tag, - ID: id, - Data: data, - } - payload, err := json.Marshal(msg) + blob, err := jsBlob.New([]any{jsScript}, map[string]any{ + "type": "text/javascript", + }) if err != nil { - jww.FATAL.Panicf("[WW] [%s] Main failed to marshal %T for %q and "+ - "ID %d going to worker: %+v", m.name, msg, tag, id, err) + return nil, err } - - 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 js.Value) { - m.receiveQueue <- data -} - -// processReceivedMessage processes the message received from the worker and -// calls the associated callback. This functions blocks until the callback -// returns. -func (m *Manager) processReceivedMessage(data []byte) error { - var msg Message - err := json.Unmarshal(data, &msg) + objectURL, err := jsURL.Call("createObjectURL", blob) if err != nil { - return err - } - - if m.messageLogging { - jww.DEBUG.Printf("[WW] [%s] Main received message for %q and ID %d "+ - "with data: %s", m.name, msg.Tag, msg.ID, msg.Data) + return nil, err } - - callback, err := m.getCallback(msg.Tag, msg.ID, msg.DeleteCB) + objectURLStr, err := objectURL.String() if err != nil { - return err + return nil, err } - callback(msg.Data) - - return nil + return NewManager(objectURLStr, name, messageLogging) } -// getCallback returns the callback for the given ID or returns an error if no -// callback is found. The callback is deleted from the map if specified in the -// message. This function is thread safe. -func (m *Manager) getCallback( - tag Tag, id uint64, deleteCB bool) (ReceptionCallback, error) { - m.mux.Lock() - defer m.mux.Unlock() - callbacks, exists := m.callbacks[tag] - if !exists { - return nil, errors.Errorf("no callbacks found for tag %q", tag) - } - - callback, exists := callbacks[id] - if !exists { - return nil, errors.Errorf("no %q callback found for ID %d", tag, id) - } - - if deleteCB { - delete(m.callbacks[tag], id) - if len(m.callbacks[tag]) == 0 { - delete(m.callbacks, tag) - } - } +// Stop closes the worker manager and terminates the worker. +func (m *Manager) Stop() error { + m.mm.Stop() - return callback, nil + // Terminate the worker + err := m.w.Terminate() + return errors.Wrapf(err, "failed to terminate worker %q", m.mm.name) } -// RegisterCallback registers the reception callback for the given tag. If a -// previous callback was registered, it is overwritten. This function is thread -// safe. -func (m *Manager) RegisterCallback(tag Tag, receptionCB ReceptionCallback) { - m.mux.Lock() - defer m.mux.Unlock() - - id := initID - - jww.DEBUG.Printf("[WW] [%s] Main registering callback for tag %q and ID %d", - m.name, tag, id) - - m.callbacks[tag] = map[uint64]ReceptionCallback{id: receptionCB} +// SendMessage sends a message to the worker with the given tag and waits for a +// response. An error is returned on failure to send or on timeout. +func (m *Manager) SendMessage(tag Tag, data []byte) (response []byte, err error) { + return m.mm.Send(tag, data) } -// RegisterCallback registers the reception callback for the given tag and a new -// unique ID used to associate the reply to the callback. Returns the ID that -// was registered. If a previous callback was registered, it is overwritten. -// This function is thread safe. -func (m *Manager) registerReplyCallback( - tag Tag, receptionCB ReceptionCallback) uint64 { - m.mux.Lock() - defer m.mux.Unlock() - id := m.getNextID(tag) - - jww.DEBUG.Printf("[WW] [%s] Main registering callback for tag %q and ID %d", - m.name, tag, id) - - if _, exists := m.callbacks[tag]; !exists { - m.callbacks[tag] = make(map[uint64]ReceptionCallback) - } - m.callbacks[tag][id] = receptionCB - - return id +// SendTimeout sends a message to the worker with the given tag and waits for a +// response. An error is returned on failure to send or on the specified +// timeout. +func (m *Manager) SendTimeout( + tag Tag, data []byte, timeout time.Duration) (response []byte, err error) { + return m.mm.SendTimeout(tag, data, timeout) } -// getNextID returns the next unique ID for the given tag. This function is not -// thread-safe. -func (m *Manager) getNextID(tag Tag) uint64 { - if _, exists := m.responseIDs[tag]; !exists { - m.responseIDs[tag] = initID - } +// SendNoResponse sends a message to the worker with the given tag. It returns +// immediately and does not wait for a response. +func (m *Manager) SendNoResponse(tag Tag, data []byte) error { + return m.mm.SendNoResponse(tag, data) +} - id := m.responseIDs[tag] - m.responseIDs[tag]++ - return id +// RegisterCallback registers the callback for the given tag. Previous tags are +// overwritten. This function is thread safe. +func (m *Manager) RegisterCallback(tag Tag, receiverCB ReceiverCallback) { + m.mm.RegisterCallback(tag, receiverCB) } -// GetWorker returns the web worker object. This returned so the worker object -// can be returned to the Javascript layer for it to communicate with the worker -// thread. -func (m *Manager) GetWorker() js.Value { return m.worker } +// GetWorker returns the Worker wrapper for the Worker Javascript object. This +// is returned so the worker object can be returned to the Javascript layer for +// it to communicate with the worker thread. +func (m *Manager) GetWorker() js.Value { return safejs.Unsafe(m.w.Value) } // Name returns the name of the web worker object. -func (m *Manager) Name() string { return m.name } +func (m *Manager) Name() string { return m.mm.name } //////////////////////////////////////////////////////////////////////////////// -// Javascript Call Wrappers // +// Worker Wrapper // //////////////////////////////////////////////////////////////////////////////// -// addEventListeners adds the event listeners needed to receive a message from -// the worker. Two listeners were added; one to receive messages from the worker -// and the other to receive errors. -func (m *Manager) addEventListeners() { - // Create a listener for when the message event is fired on the worker. This - // 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(args[0].Get("data")) - return nil - }) - - // Create listener for when an error event is fired on the worker. This - // occurs when an error occurs in the worker. - // 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.FATAL.Panicf("[WW] [%s] Main received error event: %+v", - m.name, js.Error{Value: event}) - return nil - }) - - // Create listener for when a messageerror event is fired on the worker. - // This occurs when it receives a message that cannot be deserialized. - // 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: %+v", - m.name, js.Error{Value: event}) - return nil - }) - - // Register each event listener on the worker using addEventListener - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener - m.worker.Call("addEventListener", "message", messageEvent) - m.worker.Call("addEventListener", "error", errorEvent) - m.worker.Call("addEventListener", "messageerror", messageerrorEvent) +// Worker wraps a Javascript Worker object. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker +type Worker struct { + MessagePort } -// postMessage sends a message to the worker. -// -// 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. +var ( + jsWorker = safejs.MustGetGlobal("Worker") + jsMessageChannel = safejs.MustGetGlobal("MessageChannel") + jsURL = safejs.MustGetGlobal("URL") + jsBlob = safejs.MustGetGlobal("Blob") +) + +// NewWorker creates a Javascript Worker object that executes the script at the +// specified URL. // -// 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. +// It returns any thrown exceptions as errors. // -// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage -func (m *Manager) postMessage(msg []byte) { - buffer := utils.CopyBytesToJS(msg) - m.worker.Call("postMessage", buffer, []any{buffer.Get("buffer")}) +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker +func NewWorker(aURL string, options map[string]any) (w Worker, err error) { + v, err := jsWorker.New(aURL, options) + if err != nil { + return Worker{}, err + } + + mp, err := NewMessagePort(v) + if err != nil { + return Worker{}, err + } + + return Worker{MessagePort: mp}, nil } -// terminate immediately terminates the Worker. This does not offer the worker +// Terminate immediately terminates the Worker. This does not offer the worker // an opportunity to finish its operations; it is stopped at once. // // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/terminate -func (m *Manager) terminate() { - m.worker.Call("terminate") +func (w Worker) Terminate() error { + _, err := w.Call("terminate") + return err } // newWorkerOptions creates a new Javascript object containing optional @@ -377,8 +196,8 @@ func (m *Manager) terminate() { // // Each property is optional; leave a property empty to use the defaults (as // documented). The available properties are: -// - type - The type of worker to create. The value can be either "classic" or -// "module". If not specified, the default used is "classic". +// - workerType - The type of worker to create. The value can be either +// "classic" or "module". If not specified, the default used is "classic". // - credentials - The type of credentials to use for the worker. The value // can be "omit", "same-origin", or "include". If it is not specified, or if // the type is "classic", then the default used is "omit" (no credentials @@ -387,7 +206,7 @@ func (m *Manager) terminate() { // purposes. // // Doc: https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker#options -func newWorkerOptions(workerType, credentials, name string) js.Value { +func newWorkerOptions(workerType, credentials, name string) map[string]any { options := make(map[string]any, 3) if workerType != "" { options["type"] = workerType @@ -398,5 +217,5 @@ func newWorkerOptions(workerType, credentials, name string) js.Value { if name != "" { options["name"] = name } - return js.ValueOf(options) + return options } diff --git a/worker/manager_test.go b/worker/manager_test.go index 49a395a9c45a33f25892cc1efc86e263ccf06063..67f5320d87fb977942a922f15e6e6ff4147fd09e 100644 --- a/worker/manager_test.go +++ b/worker/manager_test.go @@ -10,201 +10,39 @@ package worker import ( - "encoding/json" - "reflect" + "syscall/js" "testing" - "time" ) -// Tests Manager.processReceivedMessage calls the expected callback. -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{}) - cb := func([]byte) { cbChan <- struct{}{} } - m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb} - - data, err := json.Marshal(msg) - if err != nil { - t.Fatalf("Failed to JSON marshal Message: %+v", err) - } - - 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) - } -} - -// Tests Manager.getCallback returns the expected callback and deletes only the -// given callback when deleteCB is true. -func TestManager_getCallback(t *testing.T) { - m := &Manager{callbacks: make(map[Tag]map[uint64]ReceptionCallback)} - - // Add new callback and check that it is returned by getCallback - tag, id1 := readyTag, uint64(5) - cb := func([]byte) {} - m.callbacks[tag] = map[uint64]ReceptionCallback{id1: cb} - - received, err := m.getCallback(tag, id1, false) - if err != nil { - t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id1, err) - } - - if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(received).Pointer() { - t.Errorf("Wrong callback.\nexpected: %p\nreceived: %p", cb, received) - } - - // Add new callback under the same tag but with deleteCB set to true and - // check that it is returned by getCallback and that it was deleted from the - // map while id1 was not - id2 := uint64(56) - cb = func([]byte) {} - m.callbacks[tag][id2] = cb - - received, err = m.getCallback(tag, id2, true) - if err != nil { - t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id2, err) - } - - if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(received).Pointer() { - t.Errorf("Wrong callback.\nexpected: %p\nreceived: %p", cb, received) - } - - received, err = m.getCallback(tag, id1, false) - if err != nil { - t.Errorf("getCallback error for tag %q and ID %d: %+v", tag, id1, err) - } - - received, err = m.getCallback(tag, id2, true) - if err == nil { - t.Errorf("getCallback did not get error when trying to get deleted "+ - "callback for tag %q and ID %d", tag, id2) - } -} - -// Tests that Manager.RegisterCallback registers a callback that is then called -// by Manager.processReceivedMessage. -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{}) - cb := func([]byte) { cbChan <- struct{}{} } - m.RegisterCallback(msg.Tag, cb) - - data, err := json.Marshal(msg) - if err != nil { - t.Fatalf("Failed to JSON marshal Message: %+v", err) - } - - 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) - } -} - -// Tests that Manager.registerReplyCallback registers a callback that is then -// called by Manager.processReceivedMessage. -func TestManager_registerReplyCallback(t *testing.T) { - m := &Manager{ - callbacks: make(map[Tag]map[uint64]ReceptionCallback), - responseIDs: make(map[Tag]uint64), - } - - msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}) - cb := func([]byte) { cbChan <- struct{}{} } - m.registerReplyCallback(msg.Tag, cb) - m.callbacks[msg.Tag] = map[uint64]ReceptionCallback{msg.ID: cb} - - data, err := json.Marshal(msg) - if err != nil { - t.Fatalf("Failed to JSON marshal Message: %+v", err) - } - - 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) - } -} - -// Tests that Manager.getNextID returns the expected ID for various Tags. -func TestManager_getNextID(t *testing.T) { - m := &Manager{ - callbacks: make(map[Tag]map[uint64]ReceptionCallback), - responseIDs: make(map[Tag]uint64), - } - - for _, tag := range []Tag{readyTag, "test", "A", "B", "C"} { - id := m.getNextID(tag) - if id != initID { - t.Errorf("ID for new tag %q is not initID."+ - "\nexpected: %d\nreceived: %d", tag, initID, id) - } - - for j := uint64(1); j < 100; j++ { - id = m.getNextID(tag) - if id != j { - t.Errorf("Unexpected ID for tag %q."+ - "\nexpected: %d\nreceived: %d", tag, j, id) - } - } - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Javascript Call Wrappers // -//////////////////////////////////////////////////////////////////////////////// - // Tests that newWorkerOptions returns a Javascript object with the expected // type, credentials, and name fields. func Test_newWorkerOptions(t *testing.T) { - for i, workerType := range []string{"classic", "module"} { - for j, credentials := range []string{"omit", "same-origin", "include"} { - for k, name := range []string{"name1", "name2", "name3"} { + for _, workerType := range []string{"classic", "module"} { + for _, credentials := range []string{"omit", "same-origin", "include"} { + for _, name := range []string{"name1", "name2", "name3"} { opts := newWorkerOptions(workerType, credentials, name) - v := opts.Get("type").String() - if v != workerType { - t.Errorf("Unexpected type (%d, %d, %d)."+ - "\nexpected: %s\nreceived: %s", i, j, k, workerType, v) + optsJS := js.ValueOf(opts) + + typeJS := optsJS.Get("type").String() + if typeJS != workerType { + t.Errorf("Unexected type (type:%s credentials:%s name:%s)"+ + "\nexpected: %s\nreceived: %s", + workerType, credentials, name, workerType, typeJS) } - v = opts.Get("credentials").String() - if v != credentials { - t.Errorf("Unexpected credentials (%d, %d, %d)."+ - "\nexpected: %s\nreceived: %s", i, j, k, credentials, v) + credentialsJS := optsJS.Get("credentials").String() + if typeJS != workerType { + t.Errorf("Unexected credentials (type:%s credentials:%s "+ + "name:%s)\nexpected: %s\nreceived: %s", workerType, + credentials, name, credentials, credentialsJS) } - v = opts.Get("name").String() - if v != name { - t.Errorf("Unexpected name (%d, %d, %d)."+ - "\nexpected: %s\nreceived: %s", i, j, k, name, v) + nameJS := optsJS.Get("name").String() + if typeJS != workerType { + t.Errorf("Unexected name (type:%s credentials:%s name:%s)"+ + "\nexpected: %s\nreceived: %s", + workerType, credentials, name, name, nameJS) } } } diff --git a/worker/message.go b/worker/message.go index 3d3bc23f131ef96cdc97a3e8684d1386b07cbeb2..c9de4c1d65b3674eacde808f8d776eb2085c6dbb 100644 --- a/worker/message.go +++ b/worker/message.go @@ -14,6 +14,6 @@ package worker type Message struct { Tag Tag `json:"tag"` ID uint64 `json:"id"` - DeleteCB bool `json:"deleteCB"` + Response bool `json:"response"` Data []byte `json:"data"` } diff --git a/worker/messageChannel.go b/worker/messageChannel.go new file mode 100644 index 0000000000000000000000000000000000000000..75de2938e38ff9bbb3ff4f0a3f3090fa91027f01 --- /dev/null +++ b/worker/messageChannel.go @@ -0,0 +1,103 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 worker + +import ( + "github.com/hack-pad/safejs" + "github.com/pkg/errors" + + "gitlab.com/elixxir/wasm-utils/utils" +) + +// MessageChannel wraps a Javascript MessageChannel object. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel +type MessageChannel struct { + safejs.Value +} + +// NewMessageChannel returns a new MessageChannel object with two new +// MessagePort objects. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel/MessageChannel +func NewMessageChannel() (MessageChannel, error) { + v, err := jsMessageChannel.New() + if err != nil { + return MessageChannel{}, err + } + return MessageChannel{v}, nil +} + +// CreateMessageChannel creates a new Javascript MessageChannel between two +// workers. The [Channel] tag will be used as the prefix in the name of the +// MessageChannel when printing to logs. The key is used to look up the callback +// registered on the worker to handle the MessageChannel creation. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel +func CreateMessageChannel(w1, w2 *Manager, channelName, key string) error { + // Create a Javascript MessageChannel + mc, err := NewMessageChannel() + if err != nil { + return err + } + channelNameJS := utils.CopyBytesToJS([]byte(channelName)) + keyJS := utils.CopyBytesToJS([]byte(key)) + + port1, err := mc.Port1() + if err != nil { + return errors.Wrap(err, "could not get port1") + } + + port2, err := mc.Port2() + if err != nil { + return errors.Wrap(err, "could not get port2") + } + + obj1 := map[string]any{ + "port": port1.Value, "channel": channelNameJS, "key": keyJS} + err = w1.w.PostMessageTransfer(obj1, port1.Value) + if err != nil { + return errors.Wrap(err, "failed to send port1") + } + + obj2 := map[string]any{ + "port": port2.Value, "channel": channelNameJS, "key": keyJS} + err = w2.w.PostMessageTransfer(obj2, port2.Value) + if err != nil { + return errors.Wrap(err, "failed to send port2") + } + + return nil +} + +// Port1 returns the first port of the message channel — the port attached to +// the context that originated the channel. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel/port1 +func (mc MessageChannel) Port1() (MessagePort, error) { + v, err := mc.Get("port1") + if err != nil { + return MessagePort{}, err + } + return NewMessagePort(v) +} + +// Port2 returns the second port of the message channel — the port attached to +// the context at the other end of the channel, which the message is initially +// sent to. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel/port2 +func (mc MessageChannel) Port2() (MessagePort, error) { + v, err := mc.Get("port2") + if err != nil { + return MessagePort{}, err + } + return NewMessagePort(v) +} diff --git a/worker/messageEvent.go b/worker/messageEvent.go new file mode 100644 index 0000000000000000000000000000000000000000..cf924a2848ddfc5f47554f1e280a1f3655b8ec98 --- /dev/null +++ b/worker/messageEvent.go @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 worker + +import ( + "github.com/hack-pad/safejs" + "github.com/pkg/errors" +) + +// MessageEvent is received from the channel returned by Listen(). +// Represents a JS MessageEvent. +type MessageEvent struct { + data safejs.Value + err error + target MessagePort +} + +// Data returns this event's data or a parse error +func (e MessageEvent) Data() (safejs.Value, error) { + return e.data, errors.Wrapf(e.err, "failed to parse MessageEvent %+v", e.data) +} + +func parseMessageEvent(v safejs.Value) MessageEvent { + value, err := v.Get("target") + if err != nil { + return MessageEvent{err: err} + } + + target, err := NewMessagePort(value) + if err != nil { + return MessageEvent{err: err} + } + + data, err := v.Get("data") + if err != nil { + return MessageEvent{err: err} + } + + return MessageEvent{ + data: data, + target: target, + } +} diff --git a/worker/messageManager.go b/worker/messageManager.go new file mode 100644 index 0000000000000000000000000000000000000000..e70369c3829e886a4d585d6a3eabf430c2152d2c --- /dev/null +++ b/worker/messageManager.go @@ -0,0 +1,419 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 worker + +import ( + "context" + "encoding/json" + "fmt" + "sync" + "syscall/js" + "time" + + "github.com/aquilax/truncate" + "github.com/hack-pad/safejs" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/wasm-utils/exception" + "gitlab.com/elixxir/wasm-utils/utils" +) + +// SenderCallback is called when the sender of a message gets a response. The +// message is the response from the receiver. +type SenderCallback func(message []byte) + +// ReceiverCallback is called when receiving a message from the sender. Reply +// can optionally be used to send a response to the caller, triggering the +// [SenderCallback]. +type ReceiverCallback func(message []byte, reply func(message []byte)) + +// NewPortCallback is called with a MessagePort Javascript object when received. +type NewPortCallback func(port js.Value, channelName string) + +// MessageManager manages the sending and receiving of messages to a remote +// browser context (e.g., Worker and MessagePort) +type MessageManager struct { + // The underlying Javascript object that sends and receives messages. + p MessagePort + + // senderCallbacks are a list of SenderCallback that are called when + // receiving a response. The uint64 is a unique ID that connects each + // received reply to its original message. + senderCallbacks map[Tag]map[uint64]SenderCallback + + // receiverCallbacks are a list of ReceiverCallback that are called when + // receiving a message. + receiverCallbacks map[Tag]ReceiverCallback + + // responseIDs is a list of the newest ID to assign to each senderCallbacks + // when registered. The IDs are used to connect a reply to the original + // message. + responseIDs map[Tag]uint64 + + // messageChannelCB is a list of callbacks that are called when a new + // message channel is received. + messageChannelCB map[string]NewPortCallback + + // quit, when triggered, stops the thread that processes received messages. + quit chan struct{} + + // name names the underlying Javascript object. It is used for debugging and + // logging purposes. + name string + + Params + + mux sync.Mutex +} + +// NewMessageManager generates a new MessageManager. This functions will only +// return once communication with the remote thread has been established. +// TODO: test +func NewMessageManager( + v safejs.Value, name string, p Params) (*MessageManager, error) { + mm := initMessageManager(name, p) + mp, err := NewMessagePort(v) + if err != nil { + return nil, errors.Wrap(err, "invalid MessagePort value") + } + mm.p = mp + + ctx, cancel := context.WithCancel(context.Background()) + events, err := mm.p.Listen(ctx) + if err != nil { + cancel() + return nil, err + } + + // Start thread to process responses + go mm.messageReception(events, cancel) + + return mm, nil +} + +// initMessageManager initialises a new empty MessageManager. +func initMessageManager(name string, p Params) *MessageManager { + return &MessageManager{ + senderCallbacks: make(map[Tag]map[uint64]SenderCallback), + receiverCallbacks: make(map[Tag]ReceiverCallback), + responseIDs: make(map[Tag]uint64), + messageChannelCB: make(map[string]NewPortCallback), + quit: make(chan struct{}), + name: name, + Params: p, + } +} + +// Send sends the data to the remote thread with the given tag and waits for a +// response. Returns an error if calling postMessage throws an exception, +// marshalling the message to send fails, or if receiving a response times out. +// +// It is preferable to use [Send] over [SendNoResponse] as it will report a +// timeout when the remote thread crashes and [SendNoResponse] will not. +// TODO: test +func (mm *MessageManager) Send(tag Tag, data []byte) (response []byte, err error) { + return mm.SendTimeout(tag, data, mm.ResponseTimeout) +} + +// SendTimeout sends the data to the remote thread with a custom timeout. Refer +// to [Send] for more information. +// TODO: test +func (mm *MessageManager) SendTimeout( + tag Tag, data []byte, timeout time.Duration) (response []byte, err error) { + responseCh := make(chan []byte) + id := mm.registerSenderCallback(tag, func(msg []byte) { responseCh <- msg }) + + err = mm.sendMessage(tag, id, data) + if err != nil { + return nil, err + } + + select { + case response = <-responseCh: + return response, nil + case <-time.After(timeout): + return nil, + errors.Errorf("timed out after %s waiting for response", timeout) + } +} + +// SendNoResponse sends the data to the remote thread with the given tag; +// however, unlike [Send], it returns immediately and does not wait for a +// response. Returns an error if calling postMessage throws an exception, +// marshalling the message to send fails, or if receiving a response times out. +// +// It is preferable to use [Send] over [SendNoResponse] as it will report a +// timeout when the remote thread crashes and [SendNoResponse] will not. +// TODO: test +func (mm *MessageManager) SendNoResponse(tag Tag, data []byte) error { + return mm.sendMessage(tag, initID, data) +} + +// sendMessage packages the data into a Message with the tag and ID and sends it +// to the remote thread. +// TODO: test +func (mm *MessageManager) sendMessage(tag Tag, id uint64, data []byte) error { + if mm.MessageLogging { + jww.DEBUG.Printf("[WW] [%s] Sending message for %q and ID %d: %s", + mm.name, tag, id, truncate.Truncate( + fmt.Sprintf("%q", data), 64, "...", truncate.PositionMiddle)) + } + + msg := Message{ + Tag: tag, + ID: id, + Response: false, + Data: data, + } + payload, err := json.Marshal(msg) + if err != nil { + return err + } + + return mm.p.PostMessageTransferBytes(payload) +} + +// sendResponse sends a reply to the remote thread with the given tag and ID. +// TODO: test +func (mm *MessageManager) sendResponse(tag Tag, id uint64, data []byte) error { + if mm.MessageLogging { + jww.DEBUG.Printf("[WW] [%s] Sending reply for %q and ID %d: %s", + mm.name, tag, id, truncate.Truncate( + fmt.Sprintf("%q", data), 64, "...", truncate.PositionMiddle)) + } + + msg := Message{ + Tag: tag, + ID: id, + Response: true, + Data: data, + } + + payload, err := json.Marshal(msg) + if err != nil { + return err + } + + return mm.p.PostMessageTransferBytes(payload) +} + +// messageReception processes received messages sequentially. +// TODO: test +func (mm *MessageManager) messageReception( + events <-chan MessageEvent, cancel context.CancelFunc) { + jww.INFO.Printf("[WW] [%s] Starting message reception thread.", mm.name) + for { + select { + case <-mm.quit: + cancel() + jww.INFO.Printf( + "[WW] [%s] Quitting message reception thread.", mm.name) + return + case event := <-events: + + safeData, err := event.Data() + if err != nil { + exception.Throwf("Failed to process message: %+v", err) + } + data := safejs.Unsafe(safeData) + + switch data.Type() { + case js.TypeObject: + if data.Get("constructor").Equal(utils.Uint8Array) { + err = mm.processReceivedMessage(utils.CopyBytesToGo(data)) + if err != nil { + jww.ERROR.Printf("[WW] [%s] Failed to process "+ + "received message: %+v", mm.name, err) + } + break + } else if port := data.Get("port"); port.Truthy() { + err = mm.processReceivedPort(port, data) + if err != nil { + jww.ERROR.Printf("[WW] [%s] Failed to process "+ + "received MessagePort: %+v", mm.name, err) + } + break + } + fallthrough + + default: + jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %q: %s", + mm.name, data.Type(), utils.JsToJson(data)) + } + } + } +} + +// processReceivedMessage processes the received message and calls the +// associated callback. This functions blocks until the callback returns. +func (mm *MessageManager) processReceivedMessage(data []byte) error { + var msg Message + err := json.Unmarshal(data, &msg) + if err != nil { + return err + } + + if mm.MessageLogging { + jww.DEBUG.Printf("[WW] [%s] Received message for %q and ID %d "+ + "with data: %s", mm.name, msg.Tag, msg.ID, truncate.Truncate( + fmt.Sprintf("%q", data), 64, "...", truncate.PositionMiddle)) + } + + if msg.Response { + callback, err := mm.getSenderCallback(msg.Tag, msg.ID) + if err != nil { + return err + } + + callback(msg.Data) + } else { + callback, err := mm.getReceiverCallback(msg.Tag) + if err != nil { + return err + } + + callback(msg.Data, func(message []byte) { + if err = mm.sendResponse(msg.Tag, msg.ID, message); err != nil { + jww.FATAL.Panicf("[WW] [%s] Failed to send response for %q "+ + "and ID %d: %+v", mm.name, msg.Tag, msg.ID, err) + } + }) + } + + return nil +} + +// processReceivedPort processes the received Javascript MessagePort and calls +// the associated NewPortCallback callback. This functions blocks until the +// callback returns. +func (mm *MessageManager) processReceivedPort(port js.Value, data js.Value) error { + channel := string(utils.CopyBytesToGo(data.Get("channel"))) + key := string(utils.CopyBytesToGo(data.Get("key"))) + + jww.INFO.Printf("[WW] [%s] Received new MessageChannel %q for key %q.", + mm.name, channel, key) + + mm.mux.Lock() + cb, exists := mm.messageChannelCB[key] + mm.mux.Unlock() + + if !exists { + return errors.Errorf( + "Failed to find callback for channel %q and key %q.", channel, key) + } else { + cb(port, channel) + } + + return nil +} + +// RegisterCallback registers the callback for the given tag. Previous tags are +// overwritten. This function is thread safe. +func (mm *MessageManager) RegisterCallback(tag Tag, receiverCB ReceiverCallback) { + mm.mux.Lock() + defer mm.mux.Unlock() + + jww.DEBUG.Printf("[WW] [%s] Registering receiver callback for tag %q", + mm.name, tag) + + mm.receiverCallbacks[tag] = receiverCB +} + +// getReceiverCallback returns the ReceiverCallback for the given Tag or returns +// an error if no callback is found. This function is thread safe. +func (mm *MessageManager) getReceiverCallback(tag Tag) (ReceiverCallback, error) { + mm.mux.Lock() + defer mm.mux.Unlock() + + callback, exists := mm.receiverCallbacks[tag] + if !exists { + return nil, errors.Errorf("no receiver callbacks found for tag %q", tag) + } + + return callback, nil +} + +// registerSenderCallback registers the callback for the given tag and a new +// unique ID used to associate the reply to the callback. Returns the ID that +// was registered. If a previous callback was registered, it is overwritten. +// This function is thread safe. +func (mm *MessageManager) registerSenderCallback( + tag Tag, senderCB SenderCallback) uint64 { + mm.mux.Lock() + defer mm.mux.Unlock() + id := mm.getNextID(tag) + + jww.DEBUG.Printf("[WW] [%s] Registering callback for tag %q and ID %d", + mm.name, tag, id) + + if _, exists := mm.senderCallbacks[tag]; !exists { + mm.senderCallbacks[tag] = make(map[uint64]SenderCallback) + } + mm.senderCallbacks[tag][id] = senderCB + + return id +} + +// getSenderCallback returns the SenderCallback for the given Tag and ID or +// returns an error if no callback is found. The callback is deleted from the +// map once found. This function is thread safe. +func (mm *MessageManager) getSenderCallback( + tag Tag, id uint64) (SenderCallback, error) { + mm.mux.Lock() + defer mm.mux.Unlock() + callbacks, exists := mm.senderCallbacks[tag] + if !exists { + return nil, errors.Errorf("no sender callbacks found for tag %q", tag) + } + + callback, exists := callbacks[id] + if !exists { + return nil, + errors.Errorf("no %q sender callback found for ID %d", tag, id) + } + + delete(mm.senderCallbacks[tag], id) + if len(mm.senderCallbacks[tag]) == 0 { + delete(mm.senderCallbacks, tag) + } + + return callback, nil +} + +// RegisterMessageChannelCallback registers a callback that will be called when +// a MessagePort with the given Channel is received. +func (mm *MessageManager) RegisterMessageChannelCallback( + key string, fn NewPortCallback) { + mm.mux.Lock() + defer mm.mux.Unlock() + mm.messageChannelCB[key] = fn +} + +// Stop closes the message reception thread and closes the port. +// TODO: test +func (mm *MessageManager) Stop() { + // Stop messageReception + select { + case mm.quit <- struct{}{}: + } +} + +// getNextID returns the next unique ID for the given tag. This function is not +// thread-safe. +func (mm *MessageManager) getNextID(tag Tag) uint64 { + if _, exists := mm.responseIDs[tag]; !exists { + mm.responseIDs[tag] = initID + } + + id := mm.responseIDs[tag] + mm.responseIDs[tag]++ + return id +} diff --git a/worker/messageManager_test.go b/worker/messageManager_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bd01f64a3a50b3d1d246fa8cd85c04eda94a4899 --- /dev/null +++ b/worker/messageManager_test.go @@ -0,0 +1,344 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 worker + +import ( + "encoding/json" + "github.com/hack-pad/safejs" + "gitlab.com/elixxir/wasm-utils/utils" + "reflect" + "strconv" + "syscall/js" + "testing" + "time" +) + +func TestNewMessageManager(t *testing.T) { +} + +// Unit test of initMessageManager. +func Test_initMessageManager(t *testing.T) { + expected := &MessageManager{ + senderCallbacks: make(map[Tag]map[uint64]SenderCallback), + receiverCallbacks: make(map[Tag]ReceiverCallback), + responseIDs: make(map[Tag]uint64), + messageChannelCB: make(map[string]NewPortCallback), + quit: make(chan struct{}), + name: "name", + Params: DefaultParams(), + } + + received := initMessageManager(expected.name, expected.Params) + + received.quit = expected.quit + if !reflect.DeepEqual(expected, received) { + t.Errorf("Unexpected MessageManager.\nexpected: %+v\nreceived: %+v", + expected, received) + } +} + +func TestMessageManager_Send(t *testing.T) { +} + +func TestMessageManager_SendTimeout(t *testing.T) { +} + +func TestMessageManager_SendNoResponse(t *testing.T) { +} + +func TestMessageManager_sendMessage(t *testing.T) { +} + +func TestMessageManager_sendResponse(t *testing.T) { +} + +func TestMessageManager_messageReception(t *testing.T) { +} + +// Tests MessageManager.processReceivedMessage calls the expected callback. +func TestMessageManager_processReceivedMessage(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + msg := Message{Tag: readyTag, ID: 5} + cbChan := make(chan struct{}) + cb := func([]byte, func([]byte)) { cbChan <- struct{}{} } + mm.RegisterCallback(msg.Tag, cb) + + data, err := json.Marshal(msg) + if err != nil { + t.Fatalf("Failed to JSON marshal Message: %+v", err) + } + + go func() { + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + err = mm.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } + + msg = Message{Tag: "tag", Response: true} + cbChan = make(chan struct{}) + cb2 := func([]byte) { cbChan <- struct{}{} } + mm.registerSenderCallback(msg.Tag, cb2) + + go func() { + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + err = mm.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } +} + +// Tests MessageManager.processReceivedPort calls the expected callback. +func TestMessageManager_processReceivedPort(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + cbChan := make(chan string) + cb := func(port js.Value, channelName string) { cbChan <- channelName } + key := "testKey" + channelName := "channel" + mm.RegisterMessageChannelCallback(key, cb) + + mc, err := NewMessageChannel() + if err != nil { + t.Fatal(err) + } + port1, err := mc.Port1() + if err != nil { + t.Fatalf("Failed to get port1: %+v", err) + } + + obj := map[string]any{ + "port": safejs.Unsafe(port1.Value), + "channel": utils.CopyBytesToJS([]byte(channelName)), + "key": utils.CopyBytesToJS([]byte(key))} + + go func() { + select { + case name := <-cbChan: + if channelName != name { + t.Errorf("Received incorrect channel name."+ + "\nexpected: %q\nrecieved: %q", channelName, name) + } + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + data := js.ValueOf(obj) + err = mm.processReceivedPort(data.Get("port"), data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } +} + +// Tests that MessageManager.RegisterCallback registers a callback that is then +// called by MessageManager.processReceivedMessage. +func TestMessageManager_RegisterCallback(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + msg := Message{Tag: readyTag, ID: initID} + cbChan := make(chan struct{}) + cb := func([]byte, func([]byte)) { cbChan <- struct{}{} } + mm.RegisterCallback(msg.Tag, cb) + + data, err := json.Marshal(msg) + if err != nil { + t.Fatalf("Failed to JSON marshal Message: %+v", err) + } + + go func() { + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + err = mm.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } +} + +// Tests MessageManager.getReceiverCallback returns the expected callback. +func TestMessageManager_getReceiverCallback(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + expected := make(map[Tag]ReceiverCallback) + for i := 0; i < 5; i++ { + tag := Tag("tag" + strconv.Itoa(i)) + cb := func([]byte, func([]byte)) {} + mm.RegisterCallback(tag, cb) + expected[tag] = cb + } + + for tag, cb := range expected { + r, err := mm.getReceiverCallback(tag) + if err != nil { + t.Errorf("Error getting callback for tag %q: %+v", tag, err) + } + + if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(r).Pointer() { + t.Errorf("Wrong callback for tag %q."+ + "\nexpected: %p\nreceived: %p", tag, cb, r) + } + } +} + +// Tests that MessageManager.registerSenderCallback registers a callback that is +// then called by MessageManager.processReceivedMessage. +func TestMessageManager_registerSenderCallback(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + msg := Message{Tag: readyTag, Response: true} + cbChan := make(chan struct{}) + cb := func([]byte) { cbChan <- struct{}{} } + msg.ID = mm.registerSenderCallback(msg.Tag, cb) + + data, err := json.Marshal(msg) + if err != nil { + t.Fatalf("Failed to JSON marshal Message: %+v", err) + } + + go func() { + select { + case <-cbChan: + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + err = mm.processReceivedMessage(data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } +} + +// Tests MessageManager.getSenderCallback returns the expected callback and +// deletes it. +func TestMessageManager_getSenderCallback(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + expected := make(map[Tag]map[uint64]SenderCallback) + for i := 0; i < 5; i++ { + tag := Tag("tag" + strconv.Itoa(i)) + expected[tag] = make(map[uint64]SenderCallback) + for j := 0; j < 10; j++ { + cb := func([]byte) {} + id := mm.registerSenderCallback(tag, cb) + expected[tag][id] = cb + } + } + + for tag, callbacks := range expected { + for id, cb := range callbacks { + r, err := mm.getSenderCallback(tag, id) + if err != nil { + t.Errorf("Error getting callback for tag %q and ID %d: %+v", + tag, id, err) + } + + if reflect.ValueOf(cb).Pointer() != reflect.ValueOf(r).Pointer() { + t.Errorf("Wrong callback for tag %q and ID %d."+ + "\nexpected: %p\nreceived: %p", tag, id, cb, r) + } + + // Check that the callback was deleted + if r, err = mm.getSenderCallback(tag, id); err == nil { + t.Errorf("Did not get error when for callback that should be "+ + "deleted for tag %q and ID %d: %p", tag, id, r) + } + } + if callbacks, exists := mm.senderCallbacks[tag]; exists { + t.Errorf("Empty map for tag %s not deleted: %+v", tag, callbacks) + } + } +} + +// Tests that MessageManager.RegisterMessageChannelCallback registers a callback +// that is then called by MessageManager.processReceivedPort. +func TestMessageManager_RegisterMessageChannelCallback(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + cbChan := make(chan string) + cb := func(port js.Value, channelName string) { cbChan <- channelName } + key := "testKey" + channelName := "channel" + mm.RegisterMessageChannelCallback(key, cb) + + mc, err := NewMessageChannel() + if err != nil { + t.Fatal(err) + } + port1, err := mc.Port1() + if err != nil { + t.Fatalf("Failed to get port1: %+v", err) + } + + obj := map[string]any{ + "port": safejs.Unsafe(port1.Value), + "channel": utils.CopyBytesToJS([]byte(channelName)), + "key": utils.CopyBytesToJS([]byte(key))} + + go func() { + select { + case name := <-cbChan: + if channelName != name { + t.Errorf("Received incorrect channel name."+ + "\nexpected: %q\nrecieved: %q", channelName, name) + } + case <-time.After(10 * time.Millisecond): + t.Error("Timed out waiting for callback to be called.") + } + }() + + data := js.ValueOf(obj) + err = mm.processReceivedPort(data.Get("port"), data) + if err != nil { + t.Errorf("Failed to receive message: %+v", err) + } +} + +func TestMessageManager_Stop(t *testing.T) { +} + +// Tests that MessageManager.getNextID returns the expected ID for various Tags. +func TestMessageManager_getNextID(t *testing.T) { + mm := initMessageManager("", DefaultParams()) + + for _, tag := range []Tag{readyTag, "test", "A", "B", "C"} { + id := mm.getNextID(tag) + if id != initID { + t.Errorf("ID for new tag %q is not initID."+ + "\nexpected: %d\nreceived: %d", tag, initID, id) + } + + for j := initID + 1; j < 100; j++ { + id = mm.getNextID(tag) + if id != j { + t.Errorf("Unexpected ID for tag %q."+ + "\nexpected: %d\nreceived: %d", tag, j, id) + } + } + } +} diff --git a/worker/messagePort.go b/worker/messagePort.go new file mode 100644 index 0000000000000000000000000000000000000000..10724555a9b439776bdcdb604640da2590138f4b --- /dev/null +++ b/worker/messagePort.go @@ -0,0 +1,134 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 worker + +import ( + "context" + "syscall/js" + + "github.com/hack-pad/safejs" + "github.com/pkg/errors" + + "gitlab.com/elixxir/wasm-utils/utils" +) + +// MessagePort wraps a Javascript MessagePort object. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort +type MessagePort struct { + safejs.Value +} + +// NewMessagePort wraps the given MessagePort. +func NewMessagePort(v safejs.Value) (MessagePort, error) { + method, err := v.Get("postMessage") + if err != nil { + return MessagePort{}, err + } + if method.Type() != safejs.TypeFunction { + return MessagePort{}, errors.New("postMessage is not a function") + } + return MessagePort{v}, nil +} + +// PostMessage sends a message from the port. +func (mp MessagePort) PostMessage(message any) error { + _, err := mp.Call("postMessage", message) + return err +} + +// PostMessageTransfer sends a message from the port and transfers ownership of +// objects to other browsing contexts. +func (mp MessagePort) PostMessageTransfer(message any, transfer ...any) error { + _, err := mp.Call("postMessage", message, transfer) + return err +} + +// PostMessageTransferBytes sends the message bytes from the port via transfer. +func (mp MessagePort) PostMessageTransferBytes(message []byte) error { + buffer := utils.CopyBytesToJS(message) + return mp.PostMessageTransfer(buffer, buffer.Get("buffer")) +} + +// Listen registers listeners on the MessagePort and returns all events on the +// returned channel. +func (mp MessagePort) Listen( + ctx context.Context) (_ <-chan MessageEvent, err error) { + ctx, cancel := context.WithCancel(ctx) + defer func() { + if err != nil { + cancel() + } + }() + + events := make(chan MessageEvent) + messageHandler, err := nonBlocking(func(args []safejs.Value) { + events <- parseMessageEvent(args[0]) + }) + if err != nil { + return nil, err + } + errorHandler, err := nonBlocking(func(args []safejs.Value) { + events <- MessageEvent{err: js.Error{Value: safejs.Unsafe(args[0])}} + }) + if err != nil { + return nil, err + } + messageErrorHandler, err := nonBlocking(func(args []safejs.Value) { + events <- parseMessageEvent(args[0]) + }) + if err != nil { + return nil, err + } + + go func() { + <-ctx.Done() + _, err := mp.Call("removeEventListener", "message", messageHandler) + if err == nil { + messageHandler.Release() + } + _, err = mp.Call("removeEventListener", "error", errorHandler) + if err == nil { + errorHandler.Release() + } + _, err = mp.Call("removeEventListener", "messageerror", messageErrorHandler) + if err == nil { + messageErrorHandler.Release() + } + close(events) + }() + _, err = mp.Call("addEventListener", "message", messageHandler) + if err != nil { + return nil, err + } + _, err = mp.Call("addEventListener", "error", errorHandler) + if err != nil { + return nil, err + } + _, err = mp.Call("addEventListener", "messageerror", messageErrorHandler) + if err != nil { + return nil, err + } + if start, err := mp.Get("start"); err == nil { + if truthy, err := start.Truthy(); err == nil && truthy { + if _, err := mp.Call("start"); err != nil { + return nil, err + } + } + } + return events, nil +} + +func nonBlocking(fn func(args []safejs.Value)) (safejs.Func, error) { + return safejs.FuncOf(func(_ safejs.Value, args []safejs.Value) any { + go fn(args) + return nil + }) +} diff --git a/worker/params.go b/worker/params.go new file mode 100644 index 0000000000000000000000000000000000000000..adddb34860381018d77ba28511f5cadefb7a48ac --- /dev/null +++ b/worker/params.go @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// 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 worker + +import "time" + +// Params are parameters used in the [MessageManager]. +type Params struct { + // MessageLogging indicates if a DEBUG message should be printed every time + // a message is sent or received. + MessageLogging bool + + // ResponseTimeout is the default timeout to wait for a response before + // timing out and returning an error. + ResponseTimeout time.Duration +} + +// DefaultParams returns the default parameters. +func DefaultParams() Params { + return Params{ + MessageLogging: false, + ResponseTimeout: 30 * time.Second, + } +} diff --git a/worker/tag.go b/worker/tag.go index fa458c291802c47d0ffb1970ef75202b936561a4..3bb27a08e30241b233bec63225eac3e266fda9f7 100644 --- a/worker/tag.go +++ b/worker/tag.go @@ -14,5 +14,10 @@ type Tag string // Generic tags used by all workers. const ( - readyTag Tag = "Ready" + readyTag Tag = "<WW>Ready</WW>" +) + +const ( + Channel1LogMsgChanName = "Channel1Logger" + LoggerTag = "logger" ) diff --git a/worker/thread.go b/worker/thread.go index 203b00302a97c22f4e44501e2c721b52312c14ab..b67c3464c885bf129aeffef44fef1683c44881f7 100644 --- a/worker/thread.go +++ b/worker/thread.go @@ -10,287 +10,151 @@ package worker import ( - "encoding/json" - "sync" "syscall/js" + "time" + "github.com/hack-pad/safejs" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" - - "gitlab.com/elixxir/xxdk-wasm/utils" ) // 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) +type ThreadReceptionCallback func(message []byte, reply func(message []byte)) // ThreadManager queues incoming messages from the main thread and handles them // based on their tag. type ThreadManager struct { - // messages is a list of queued messages sent from the main thread. - messages chan js.Value - - // callbacks is a list of callbacks to handle messages that come from the - // main thread keyed on the callback tag. - callbacks map[Tag]ThreadReceptionCallback - - // 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{} + mm *MessageManager - // name describes the worker. It is used for debugging and logging purposes. - name string - - // messageLogging determines if debug message logs should be printed every - // time a message is sent/received to/from the worker. - messageLogging bool - - mux sync.Mutex + // Wrapper of the DedicatedWorkerGlobalScope. + // Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope + t Thread } // NewThreadManager initialises a new ThreadManager. -func NewThreadManager(name string, messageLogging bool) *ThreadManager { - tm := &ThreadManager{ - messages: make(chan js.Value, 100), - callbacks: make(map[Tag]ThreadReceptionCallback), - receiveQueue: make(chan js.Value, receiveQueueChanSize), - quit: make(chan struct{}), - name: name, - messageLogging: messageLogging, +func NewThreadManager(name string, messageLogging bool) (*ThreadManager, error) { + t, err := NewThread() + if err != nil { + return nil, errors.Wrapf(err, "failed to construct GlobalSelf") } - // Start thread to process messages from the main thread - go tm.processThread() - - tm.addEventListeners() - return tm -} + p := DefaultParams() + p.MessageLogging = messageLogging + mm, err := NewMessageManager(t.Value, name+"-remote", p) + if err != nil { + return nil, errors.Wrapf(err, "failed to construct message manager") + } -// Stop closes the thread manager and stops the worker. -func (tm *ThreadManager) Stop() { - // Stop processThread - select { - case tm.quit <- struct{}{}: + tm := &ThreadManager{ + mm: mm, + t: t, } - // Terminate the worker - go tm.close() + return tm, nil } -// processThread processes received messages sequentially. -func (tm *ThreadManager) processThread() { - jww.INFO.Printf("[WW] [%s] Starting worker process thread.", tm.name) - for { - select { - case <-tm.quit: - jww.INFO.Printf("[WW] [%s] Quitting worker process thread.", tm.name) - return - case msgData := <-tm.receiveQueue: +// Stop closes the thread manager and stops the worker. +func (tm *ThreadManager) Stop() error { + tm.mm.Stop() - 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 + // Close the worker + err := tm.t.Close() + return errors.Wrapf(err, "failed to close worker %q", tm.mm.name) +} - default: - jww.ERROR.Printf("[WW] [%s] Cannot handle data of type %s "+ - "from main thread: %s", - tm.name, msgData.Type(), utils.JsToJson(msgData)) - } - } - } +func (tm *ThreadManager) GetWorker() js.Value { + return safejs.Unsafe(tm.t.Value) } // SignalReady sends a signal to the main thread indicating that the worker is // ready. Once the main thread receives this, it will initiate communication. // Therefore, this should only be run once all listeners are ready. func (tm *ThreadManager) SignalReady() { - tm.SendMessage(readyTag, nil) -} - -// SendMessage sends a message to the main thread for the given tag. -func (tm *ThreadManager) SendMessage(tag Tag, data []byte) { - msg := Message{ - Tag: tag, - ID: initID, - DeleteCB: false, - Data: data, - } - - if tm.messageLogging { - jww.DEBUG.Printf("[WW] [%s] Worker sending message for %q with data: %s", - tm.name, tag, data) - } - - payload, err := json.Marshal(msg) + err := tm.mm.SendNoResponse(readyTag, nil) if err != nil { - jww.FATAL.Panicf("[WW] [%s] Worker failed to marshal %T for %q going "+ - "to main: %+v", tm.name, msg, tag, err) + jww.FATAL.Panicf( + "[WW] [%s] Failed to send ready signal: %+v", tm.Name(), err) } - - go tm.postMessage(payload) } -// sendResponse sends a reply to the main thread with the given tag and ID. -func (tm *ThreadManager) sendResponse(tag Tag, id uint64, data []byte) error { - msg := Message{ - Tag: tag, - ID: id, - DeleteCB: true, - Data: data, - } - - if tm.messageLogging { - jww.DEBUG.Printf("[WW] [%s] Worker sending reply for %q and ID %d "+ - "with data: %s", tm.name, tag, id, data) - } - - payload, err := json.Marshal(msg) - if err != nil { - return errors.Errorf("worker failed to marshal %T for %q and ID "+ - "%d going to main: %+v", msg, tag, id, err) - } - - go tm.postMessage(payload) - - return nil +// RegisterMessageChannelCallback registers a callback that will be called when +// a MessagePort with the given Channel is received. +func (tm *ThreadManager) RegisterMessageChannelCallback( + tag string, fn NewPortCallback) { + tm.mm.RegisterMessageChannelCallback(tag, fn) } -// 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 js.Value) { - tm.receiveQueue <- data +// SendMessage sends a message to the main thread with the given tag and waits +// for a response. An error is returned on failure to send or on timeout. +func (tm *ThreadManager) SendMessage( + tag Tag, data []byte) (response []byte, err error) { + return tm.mm.Send(tag, data) } -// processReceivedMessage processes the message received from the main thread -// and calls the associated callback. If the registered callback returns a -// response, it is sent to the main thread. This functions blocks until the -// callback returns. -func (tm *ThreadManager) processReceivedMessage(data []byte) error { - var msg Message - err := json.Unmarshal(data, &msg) - if err != nil { - return err - } - - if tm.messageLogging { - jww.DEBUG.Printf("[WW] [%s] Worker received message for %q and ID %d "+ - "with data: %s", tm.name, msg.Tag, msg.ID, msg.Data) - } - - tm.mux.Lock() - callback, exists := tm.callbacks[msg.Tag] - tm.mux.Unlock() - if !exists { - return errors.Errorf("no callback found for tag %q", msg.Tag) - } - - // Call callback and register response with its return - response, err := callback(msg.Data) - if err != nil { - return errors.Errorf("callback for %q and ID %d returned an error: %+v", - msg.Tag, msg.ID, err) - } - if response != nil { - return tm.sendResponse(msg.Tag, msg.ID, response) - } +// SendTimeout sends a message to the main thread with the given tag and waits +// for a response. An error is returned on failure to send or on the specified +// timeout. +func (tm *ThreadManager) SendTimeout( + tag Tag, data []byte, timeout time.Duration) (response []byte, err error) { + return tm.mm.SendTimeout(tag, data, timeout) +} - return nil +// SendNoResponse sends a message to the main thread with the given tag. It +// returns immediately and does not wait for a response. +func (tm *ThreadManager) SendNoResponse(tag Tag, data []byte) error { + return tm.mm.SendNoResponse(tag, data) } -// RegisterCallback registers the callback with the given tag overwriting any -// previous registered callbacks with the same tag. This function is thread -// safe. -// -// If the callback returns anything but nil, it will be returned as a response. -func (tm *ThreadManager) RegisterCallback( - tag Tag, receptionCallback ThreadReceptionCallback) { - jww.DEBUG.Printf( - "[WW] [%s] Worker registering callback for tag %q", tm.name, tag) - tm.mux.Lock() - tm.callbacks[tag] = receptionCallback - tm.mux.Unlock() +// RegisterCallback registers the callback for the given tag. Previous tags are +// overwritten. This function is thread safe. +func (tm *ThreadManager) RegisterCallback(tag Tag, receiverCB ReceiverCallback) { + tm.mm.RegisterCallback(tag, receiverCB) } +// Name returns the name of the web worker. +func (tm *ThreadManager) Name() string { return tm.mm.name } + //////////////////////////////////////////////////////////////////////////////// // Javascript Call Wrappers // //////////////////////////////////////////////////////////////////////////////// -// addEventListeners adds the event listeners needed to receive a message from -// the worker. Two listeners were added; one to receive messages from the worker -// and the other to receive errors. -func (tm *ThreadManager) addEventListeners() { - // Create a listener for when the message event is fire on the worker. This - // 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(args[0].Get("data")) - return nil - }) +// Thread has the methods of the Javascript DedicatedWorkerGlobalScope. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope +type Thread struct { + MessagePort +} - // Create listener for when an error event is fired on the worker. This - // occurs when an error occurs in the worker. - // 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)) - return nil - }) +// NewThread creates a new Thread from Global. +func NewThread() (Thread, error) { + self, err := safejs.Global().Get("self") + if err != nil { + return Thread{}, err + } - // Create listener for when a messageerror event is fired on the worker. - // This occurs when it receives a message that cannot be deserialized. - // 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)) - return nil - }) + mp, err := NewMessagePort(self) + if err != nil { + return Thread{}, err + } - // Register each event listener on the worker using addEventListener - // Doc: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener - js.Global().Call("addEventListener", "message", messageEvent) - js.Global().Call("addEventListener", "error", errorEvent) - js.Global().Call("addEventListener", "messageerror", messageerrorEvent) + return Thread{mp}, nil } -// postMessage sends a message from this worker to the main WASM thread. +// Name returns the name that the Worker was (optionally) given when it was +// created. // -// 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 []byte) { - buffer := utils.CopyBytesToJS(aMessage) - js.Global().Call("postMessage", buffer, []any{buffer.Get("buffer")}) +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/name +func (t *Thread) Name() string { + return safejs.Unsafe(t.Value).Get("name").String() } -// close discards any tasks queued in the worker's event loop, effectively +// Close discards any tasks queued in the worker's event loop, effectively // closing this particular scope. // -// 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. -// // Doc: https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/close -func (tm *ThreadManager) close() { - js.Global().Call("close") +func (t *Thread) Close() error { + _, err := t.Call("close") + return err } diff --git a/worker/thread_test.go b/worker/thread_test.go deleted file mode 100644 index ada6de8fc00916699b45eaee1483830a277b9fc9..0000000000000000000000000000000000000000 --- a/worker/thread_test.go +++ /dev/null @@ -1,73 +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 worker - -import ( - "encoding/json" - "testing" - "time" -) - -// Tests that ThreadManager.processReceivedMessage calls the expected callback. -func TestThreadManager_processReceivedMessage(t *testing.T) { - tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)} - - msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}) - cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil } - tm.callbacks[msg.Tag] = cb - - data, err := json.Marshal(msg) - if err != nil { - t.Fatalf("Failed to JSON marshal Message: %+v", err) - } - - 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) - } -} - -// Tests that ThreadManager.RegisterCallback registers a callback that is then -// called by ThreadManager.processReceivedMessage. -func TestThreadManager_RegisterCallback(t *testing.T) { - tm := &ThreadManager{callbacks: make(map[Tag]ThreadReceptionCallback)} - - msg := Message{Tag: readyTag, ID: 5} - cbChan := make(chan struct{}) - cb := func([]byte) ([]byte, error) { cbChan <- struct{}{}; return nil, nil } - tm.RegisterCallback(msg.Tag, cb) - - data, err := json.Marshal(msg) - if err != nil { - t.Fatalf("Failed to JSON marshal Message: %+v", err) - } - - 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) - } -}