├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .scalafix.conf ├── .vscode └── settings.json ├── LICENCE ├── README.md ├── TLA+ ├── .gitignore ├── websocket_client.cfg ├── websocket_client.output.txt ├── websocket_client.tla ├── websockets.cfg ├── websockets.output.txt └── websockets.tla ├── bin └── add_source_links_to_readme ├── build.sbt ├── core ├── js │ └── src │ │ ├── main │ │ ├── scala-2 │ │ │ └── japgolly │ │ │ │ └── webapputil │ │ │ │ └── general │ │ │ │ └── internal │ │ │ │ └── MergeErrors.scala │ │ ├── scala-3 │ │ │ └── japgolly │ │ │ │ └── webapputil │ │ │ │ └── general │ │ │ │ └── internal │ │ │ │ └── MergeErrors.scala │ │ └── scala │ │ │ └── japgolly │ │ │ └── webapputil │ │ │ ├── ajax │ │ │ ├── AjaxClient.scala │ │ │ └── AjaxException.scala │ │ │ ├── binary │ │ │ ├── BinaryData_PlatformSpecific.scala │ │ │ ├── BinaryFormat.scala │ │ │ ├── BinaryJs.scala │ │ │ ├── BinaryString.scala │ │ │ ├── Compression.scala │ │ │ ├── Encryption.scala │ │ │ └── Pako.scala │ │ │ ├── browser │ │ │ ├── WindowConfirm.scala │ │ │ ├── WindowLocation.scala │ │ │ └── WindowPrompt.scala │ │ │ ├── entrypoint │ │ │ └── Entrypoint.scala │ │ │ ├── general │ │ │ ├── AsyncFunction.scala │ │ │ ├── CallbackHelpers.scala │ │ │ ├── Effect_PlatformSpecific.scala │ │ │ ├── JsExt.scala │ │ │ ├── LoggerJs.scala │ │ │ ├── TimersJs.scala │ │ │ └── VarJs.scala │ │ │ ├── http │ │ │ └── UrlEncoder.scala │ │ │ ├── indexeddb │ │ │ ├── IndexedDb.scala │ │ │ ├── IndexedDbKey.scala │ │ │ ├── KeyCodec.scala │ │ │ ├── ObjectStoreDef.scala │ │ │ ├── Txn.scala │ │ │ ├── TxnDsl.scala │ │ │ ├── TxnMode.scala │ │ │ ├── TxnStep.scala │ │ │ ├── ValueCodec.scala │ │ │ └── package.scala │ │ │ ├── locks │ │ │ ├── AbstractSharedLock.scala │ │ │ └── SharedLock.scala │ │ │ ├── websocket │ │ │ ├── WebSocket.scala │ │ │ └── WebSocketClient.scala │ │ │ ├── webstorage │ │ │ ├── AbstractWebStorage.scala │ │ │ ├── KeyCodec.scala │ │ │ ├── ValueCodec.scala │ │ │ └── WebStorageKey.scala │ │ │ └── webworker │ │ │ ├── AbstractWebWorker.scala │ │ │ ├── ManagedWebWorker.scala │ │ │ ├── OnError.scala │ │ │ └── WebWorkerProtocol.scala │ │ └── test │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ ├── binary │ │ └── BinaryDataJsTest.scala │ │ ├── general │ │ ├── AsyncFunctionTest.scala │ │ └── VarJsTest.scala │ │ └── webstorage │ │ └── WebStorageTest.scala ├── jvm │ └── src │ │ ├── main │ │ └── scala │ │ │ └── japgolly │ │ │ └── webapputil │ │ │ ├── binary │ │ │ └── BinaryData_PlatformSpecific.scala │ │ │ ├── entrypoint │ │ │ ├── EntrypointInvoker.scala │ │ │ ├── Html.scala │ │ │ ├── Js.scala │ │ │ └── LoadJs.scala │ │ │ ├── general │ │ │ ├── Effect_PlatformSpecific.scala │ │ │ └── ThreadUtils.scala │ │ │ ├── http │ │ │ └── UrlEncoder.scala │ │ │ ├── locks │ │ │ ├── LockMechanism.scala │ │ │ ├── LockUtils.scala │ │ │ └── SharedLock.scala │ │ │ └── websocket │ │ │ └── WebSocketServerUtil.scala │ │ └── test │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── entrypoint │ │ └── EntrypointInvokerTest.scala └── shared │ └── src │ ├── main │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ ├── ajax │ │ └── AjaxProtocol.scala │ │ ├── binary │ │ ├── BinaryData.scala │ │ └── CodecEngine.scala │ │ ├── entrypoint │ │ └── EntrypointDef.scala │ │ ├── general │ │ ├── AbstractMultiStringMap.scala │ │ ├── Effect.scala │ │ ├── Enabled.scala │ │ ├── ErrorMsg.scala │ │ ├── LazyVal.scala │ │ ├── MultiStringMap.scala │ │ ├── Permission.scala │ │ ├── Protocol.scala │ │ ├── Retries.scala │ │ ├── Url.scala │ │ └── Version.scala │ │ ├── http │ │ ├── Cookie.scala │ │ ├── HttpClient.scala │ │ └── UrlEncoderApi.scala │ │ ├── locks │ │ └── GenericSharedLock.scala │ │ └── websocket │ │ └── WebSocketShared.scala │ └── test │ └── scala │ └── japgolly │ └── webapputil │ ├── binary │ └── BinaryDataTest.scala │ ├── general │ └── UrlTest.scala │ └── http │ └── HttpClientTest.scala ├── coreBoopickle ├── js │ └── src │ │ ├── main │ │ └── scala │ │ │ └── japgolly │ │ │ └── webapputil │ │ │ └── boopickle │ │ │ ├── BinaryFormatExt.scala │ │ │ ├── BinaryWebWorkerProtocol.scala │ │ │ ├── BoopickleWebSocketClient.scala │ │ │ ├── EncryptionEngine.scala │ │ │ └── IndexedDbExt.scala │ │ └── test │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── boopickle │ │ ├── BinaryFormatTest.scala │ │ ├── BinaryStringTest.scala │ │ └── CompressionTest.scala ├── jvm │ └── src │ │ └── main │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── boopickle │ │ └── BinaryFormatExt.scala └── shared │ └── src │ └── main │ └── scala │ └── japgolly │ └── webapputil │ └── boopickle │ ├── BoopickleCodecEngine.scala │ ├── EntrypointDefExt.scala │ ├── PicklerUtil.scala │ ├── SafePickler.scala │ ├── SafePicklerUtil.scala │ └── package.scala ├── coreCatsEffect ├── js │ └── src │ │ └── main │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── cats │ │ └── effect │ │ ├── PlatformImplicits.scala │ │ └── WebappUtilEffectIO.scala ├── jvm │ └── src │ │ └── main │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── cats │ │ └── effect │ │ ├── PlatformImplicits.scala │ │ ├── ThreadUtilsIO.scala │ │ ├── WebappUtilEffectIO.scala │ │ └── WebappUtilIOExt.scala └── shared │ └── src │ └── main │ └── scala │ └── japgolly │ └── webapputil │ └── cats │ └── effect │ ├── Implicits.scala │ ├── WebappUtilEffectAsyncIO.scala │ └── package.scala ├── coreCirce ├── js │ └── src │ │ └── main │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── circe │ │ └── JsonAjaxClientModule.scala ├── jvm │ └── src │ │ └── main │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── circe │ │ └── JsonAjaxClientModule.scala └── shared │ └── src │ ├── main │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── circe │ │ ├── JsonCodec.scala │ │ ├── JsonCodecArityBoilerplate.scala │ │ ├── JsonEntrypointCodec.scala │ │ ├── JsonHttpClientExt.scala │ │ ├── JsonUtil.scala │ │ └── package.scala │ └── test │ └── scala │ └── japgolly │ └── webapputil │ └── circe │ └── HttpClientExtTest.scala ├── coreOkHttp4 └── src │ └── main │ └── scala │ └── japgolly │ └── webapputil │ └── okhttp4 │ └── OkHttp4Client.scala ├── dbPostgres └── src │ └── main │ └── scala │ └── japgolly │ └── webapputil │ └── db │ ├── Db.scala │ ├── DbConfig.scala │ ├── DbMigration.scala │ ├── DoobieCodecs.scala │ ├── DoobieHelpers.scala │ ├── JdbcLogging.scala │ ├── SqlTracer.scala │ └── XA.scala ├── examples ├── js │ └── src │ │ └── test │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── examples │ │ ├── ajax │ │ ├── AjaxExampleJs.scala │ │ └── AjaxExampleJsTest.scala │ │ ├── entrypoint │ │ └── EntrypointExampleFrontend.scala │ │ └── indexeddb │ │ ├── IDBExample.scala │ │ ├── IDBExampleModels.scala │ │ ├── IDBExampleStores.scala │ │ └── IDBExampleTest.scala ├── jvm │ └── src │ │ └── test │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── examples │ │ ├── ajax │ │ └── AjaxExampleJvm.scala │ │ └── entrypoint │ │ ├── EntrypointExampleBackend.scala │ │ └── EntrypointExampleBackendTest.scala └── shared │ └── src │ └── test │ └── scala │ └── japgolly │ └── webapputil │ └── examples │ ├── ajax │ └── AjaxExampleShared.scala │ └── entrypoint │ └── EntrypointExample.scala ├── ghpages └── src │ └── docs │ ├── README.md │ └── examples │ ├── ajax.md │ ├── directory.conf │ ├── entrypoint.md │ └── indexeddb.md ├── jsBundles ├── .gitignore ├── Makefile ├── dist │ └── fake-indexeddb.js ├── package.json ├── src │ └── fake-indexeddb.js ├── webpack.config.js └── yarn.lock ├── project ├── AdvancedNodeJSEnv.scala ├── Build.scala ├── Dependencies.scala ├── GenJsonCodecs.scala ├── Lib.scala ├── build.properties └── plugins.sbt ├── scalafix.sbt ├── testBoopickle └── js │ └── src │ ├── main │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── boopickle │ │ └── test │ │ ├── TestEncryption.scala │ │ ├── TestIndexedDb.scala │ │ └── WebSocketTestUtil.scala │ └── test │ └── scala │ └── japgolly │ └── webapputil │ └── boopickle │ └── test │ ├── EncryptionTest.scala │ ├── FakeIndexedDb.scala │ ├── IndexedDbTest.scala │ ├── WebSocketClientPropTest.scala │ ├── WebSocketClientTest.scala │ └── WebSocketClientTester.scala ├── testCatsEffect └── jvm │ └── src │ └── main │ └── scala │ └── japgolly │ └── webapputil │ └── cats │ └── effect │ └── test │ └── package.scala ├── testCirce ├── js │ └── src │ │ └── main │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── circe │ │ └── test │ │ └── package.scala └── shared │ └── src │ └── main │ └── scala │ └── japgolly │ └── webapputil │ └── circe │ └── test │ └── JsonTestUtil.scala ├── testCore ├── js │ └── src │ │ └── main │ │ └── scala │ │ └── japgolly │ │ └── webapputil │ │ └── test │ │ ├── TestAjaxClient.scala │ │ ├── TestState.scala │ │ ├── TestTimersJs.scala │ │ ├── TestWebSocket.scala │ │ ├── TestWebWorker.scala │ │ ├── TestWindowConfirm.scala │ │ ├── TestWindowLocation.scala │ │ └── TestWindowPrompt.scala └── shared │ └── src │ └── main │ └── scala │ └── japgolly │ └── webapputil │ └── test │ ├── BinaryTestUtil.scala │ └── TestHttpClient.scala ├── testDbPostgres └── src │ └── main │ └── scala │ └── japgolly │ └── webapputil │ └── db │ └── test │ ├── DbTable.scala │ ├── DelegateConnection.scala │ ├── ImperativeXA.scala │ ├── TestDb.scala │ ├── TestDbConfig.scala │ ├── TestDbHelpers.scala │ └── TestXA.scala └── testNode └── src └── main └── scala └── japgolly └── webapputil └── test └── node └── TestNode.scala /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: japgolly 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: github-actions 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | open-pull-requests-limit: 10 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches-ignore: 7 | - gh-pages 8 | tags-ignore: 9 | - v*.*.* 10 | 11 | jobs: 12 | 13 | ci: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | include: 19 | - java: 16 20 | scala: 2 21 | - java: 11 22 | scala: 3 23 | name: Scala v${{ matrix.scala }} / Java v${{ matrix.java }} 24 | steps: 25 | 26 | - name: Git checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Setup Scala 30 | uses: japgolly/setup-everything-scala@v3.1 31 | with: 32 | java-version: adopt@1.${{ matrix.java }} 33 | jsdom-version: 22.1.0 34 | node-version: 19.9.0 35 | 36 | - name: Build and test 37 | shell: bash 38 | run: >- 39 | sbt++field scala${{ matrix.scala }} 40 | -J-Xmx3G 41 | -J-XX:+UseG1GC 42 | -DCI=1 43 | clean test ghpages/laikaSite 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | 8 | jobs: 9 | 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - uses: actions/checkout@v3 15 | 16 | - name: Setup Scala 17 | uses: japgolly/setup-everything-scala@v3.1 18 | with: 19 | java-version: adopt@1.11 20 | jsdom-version: 22.1.0 21 | node-version: 19.9.0 22 | 23 | - name: Release 24 | run: >- 25 | sbt 26 | -J-Xmx3G 27 | -J-XX:+UseG1GC 28 | -DCI=1 29 | ci-release 30 | env: 31 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 32 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 33 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 34 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swn 2 | .*.swo 3 | .*.swp 4 | .~* 5 | .bloop 6 | .bsp 7 | .cache 8 | .classpath 9 | .idea 10 | .idea_modules 11 | .metadata 12 | .metals 13 | .project 14 | .target 15 | *.bak 16 | *.err 17 | *.log 18 | *.out 19 | *.pid 20 | *.tmp 21 | ~* 22 | project/.sbtboot/ 23 | project/metals.sbt 24 | target 25 | -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | OrganizeImports, 3 | RemoveUnused, 4 | ] 5 | 6 | RemoveUnused { 7 | imports = false 8 | privates = true 9 | locals = true 10 | } 11 | 12 | OrganizeImports { 13 | expandRelative = true 14 | groupedImports = AggressiveMerge 15 | groupExplicitlyImportedImplicitsSeparately = false 16 | groups = ["*"] 17 | importSelectorsOrder = Ascii 18 | removeUnused = true 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [ 3 | 88, 4 | 120 5 | ], 6 | "files.watcherExclude": { 7 | "**/target": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /TLA+/.gitignore: -------------------------------------------------------------------------------- 1 | *.toolbox 2 | run 3 | states 4 | -------------------------------------------------------------------------------- /TLA+/websocket_client.cfg: -------------------------------------------------------------------------------- 1 | SPECIFICATION Spec 2 | 3 | INVARIANTS TypeInvariants 4 | DataInvariants 5 | 6 | PROPERTIES Liveness -------------------------------------------------------------------------------- /TLA+/websocket_client.output.txt: -------------------------------------------------------------------------------- 1 | TLC2 Version 2.17 of 02 February 2022 (rev: 3c7caa5) 2 | Running breadth-first search Model-Checking with fp 25 and seed -2654345447334480850 with 32 workers on 32 cores with 25486MB heap and 64MB offheap memory [pid: 114128] (Linux 5.17.5-arch1-2 amd64, GraalVM Community 11.0.14 x86_64, MSBDiskFPSet, DiskStateQueue). 3 | Parsing file /home/golly/projects/public/webapp-util/TLA+/websocket_client.tla 4 | Parsing file /tmp/TLC.tla 5 | Parsing file /tmp/Naturals.tla 6 | Parsing file /tmp/Sequences.tla 7 | Parsing file /tmp/FiniteSets.tla 8 | Semantic processing of module Naturals 9 | Semantic processing of module Sequences 10 | Semantic processing of module FiniteSets 11 | Semantic processing of module TLC 12 | Semantic processing of module websocket_client 13 | Starting... (2022-05-13 08:44:43) 14 | Implied-temporal checking--satisfiability problem has 1 branches. 15 | Computing initial states... 16 | [authorised |-> TRUE, retry |-> FALSE, scheduled |-> FALSE, ws |-> "None"] 17 | [authorised |-> TRUE, retry |-> TRUE, scheduled |-> FALSE, ws |-> "None"] 18 | Finished computing initial states: 2 distinct states generated at 2022-05-13 08:44:43. 19 | [authorised |-> TRUE, retry |-> FALSE, scheduled |-> FALSE, ws |-> "Connecting"] 20 | [authorised |-> TRUE, retry |-> TRUE, scheduled |-> FALSE, ws |-> "Connecting"] 21 | [authorised |-> TRUE, retry |-> FALSE, scheduled |-> FALSE, ws |-> "Open"] 22 | [authorised |-> TRUE, retry |-> TRUE, scheduled |-> FALSE, ws |-> "Open"] 23 | [authorised |-> TRUE, retry |-> FALSE, scheduled |-> FALSE, ws |-> "Closing"] 24 | [authorised |-> TRUE, retry |-> TRUE, scheduled |-> FALSE, ws |-> "Closing"] 25 | [authorised |-> TRUE, retry |-> FALSE, scheduled |-> FALSE, ws |-> "Closed"] 26 | [authorised |-> TRUE, retry |-> FALSE, scheduled |-> TRUE, ws |-> "None"] 27 | [authorised |-> TRUE, retry |-> FALSE, scheduled |-> TRUE, ws |-> "Closed"] 28 | [authorised |-> FALSE, retry |-> FALSE, scheduled |-> FALSE, ws |-> "None"] 29 | [authorised |-> TRUE, retry |-> TRUE, scheduled |-> TRUE, ws |-> "None"] 30 | [authorised |-> TRUE, retry |-> TRUE, scheduled |-> TRUE, ws |-> "Closed"] 31 | Progress(4) at 2022-05-13 08:44:43: 76 states generated, 14 distinct states found, 0 states left on queue. 32 | Checking temporal properties for the complete state space with 14 total distinct states at (2022-05-13 08:44:43) 33 | Finished checking temporal properties in 00s at 2022-05-13 08:44:43 34 | Model checking completed. No error has been found. 35 | Estimates of the probability that TLC did not check all reachable states 36 | because two distinct states had the same fingerprint: 37 | calculated (optimistic): val = 4.7E-17 38 | 76 states generated, 14 distinct states found, 0 states left on queue. 39 | The depth of the complete state graph search is 4. 40 | The average outdegree of the complete state graph is 1 (minimum is 0, the maximum 3 and the 95th percentile is 3). 41 | Finished in 00s at (2022-05-13 08:44:43) 42 | -------------------------------------------------------------------------------- /TLA+/websockets.cfg: -------------------------------------------------------------------------------- 1 | SPECIFICATION Spec 2 | 3 | INVARIANTS TypeInvariants 4 | 5 | -------------------------------------------------------------------------------- /TLA+/websockets.output.txt: -------------------------------------------------------------------------------- 1 | TLC2 Version 2.17 of 02 February 2022 (rev: 3c7caa5) 2 | Running breadth-first search Model-Checking with fp 97 and seed -9177898650443556873 with 32 workers on 32 cores with 25486MB heap and 64MB offheap memory [pid: 114369] (Linux 5.17.5-arch1-2 amd64, GraalVM Community 11.0.14 x86_64, MSBDiskFPSet, DiskStateQueue). 3 | Parsing file /home/golly/projects/public/webapp-util/TLA+/websockets.tla 4 | Parsing file /tmp/TLC.tla 5 | Parsing file /tmp/Naturals.tla 6 | Parsing file /tmp/Sequences.tla 7 | Parsing file /tmp/FiniteSets.tla 8 | Semantic processing of module Naturals 9 | Semantic processing of module Sequences 10 | Semantic processing of module FiniteSets 11 | Semantic processing of module TLC 12 | Semantic processing of module websockets 13 | Starting... (2022-05-13 08:45:09) 14 | Computing initial states... 15 | [cli |-> "closed ", svr |-> "closed "] 16 | Finished computing initial states: 1 distinct state generated at 2022-05-13 08:45:09. 17 | [cli |-> "connecting", svr |-> "closed "] 18 | [cli |-> "closing ", svr |-> "closed "] 19 | [cli |-> "connecting", svr |-> "connecting"] 20 | [cli |-> "open ", svr |-> "connecting"] 21 | [cli |-> "closing ", svr |-> "connecting"] 22 | [cli |-> "connecting", svr |-> "open "] 23 | [cli |-> "connecting", svr |-> "closing "] 24 | [cli |-> "closed ", svr |-> "connecting"] 25 | [cli |-> "closing ", svr |-> "closing "] 26 | [cli |-> "open ", svr |-> "open "] 27 | [cli |-> "closed ", svr |-> "closing "] 28 | [cli |-> "open ", svr |-> "closing "] 29 | [cli |-> "closing ", svr |-> "open "] 30 | [cli |-> "open ", svr |-> "closed "] 31 | [cli |-> "closed ", svr |-> "open "] 32 | Model checking completed. No error has been found. 33 | Estimates of the probability that TLC did not check all reachable states 34 | because two distinct states had the same fingerprint: 35 | calculated (optimistic): val = 4.1E-17 36 | 63 states generated, 16 distinct states found, 0 states left on queue. 37 | The depth of the complete state graph search is 5. 38 | The average outdegree of the complete state graph is 1 (minimum is 0, the maximum 4 and the 95th percentile is 4). 39 | Finished in 00s at (2022-05-13 08:45:09) 40 | -------------------------------------------------------------------------------- /TLA+/websockets.tla: -------------------------------------------------------------------------------- 1 | -------------------------------------------------- MODULE websockets -------------------------------------------------- 2 | 3 | (* PURPOSE: 4 | 5 | Use cases 6 | ========= 7 | - User starts page 8 | Init WS 9 | Request initial state 10 | (retry until ok?) 11 | 12 | - User connected 13 | Updates sent 14 | Connection lost 15 | Retry until re-establish 16 | On re-establish, re-sync 17 | *) 18 | 19 | EXTENDS TLC 20 | 21 | VARIABLES cli, svr 22 | 23 | vars == << cli, svr >> 24 | 25 | connecting == "connecting" 26 | open == "open " 27 | closing == "closing " 28 | closed == "closed " 29 | 30 | WSStates == {connecting, open, closing, closed} 31 | 32 | TypeInvariants == 33 | /\ PrintT([cli |-> cli, svr |-> svr]) 34 | /\ cli \in WSStates 35 | /\ svr \in WSStates 36 | 37 | Init == 38 | /\ cli = closed 39 | /\ svr = closed 40 | 41 | ----------------------------------------------------------------------------------------- 42 | 43 | ClientConnect == 44 | /\ cli = closed 45 | /\ svr = closed 46 | /\ cli' = connecting 47 | /\ UNCHANGED svr 48 | 49 | ClientConnected == 50 | /\ cli = connecting 51 | /\ svr \in {connecting,open} 52 | /\ cli' = open 53 | /\ UNCHANGED svr 54 | 55 | ClientClose == 56 | /\ cli /= closed 57 | /\ cli' = closing 58 | /\ UNCHANGED svr 59 | 60 | ClientClosed == 61 | /\ cli' = closed 62 | /\ UNCHANGED svr 63 | 64 | Client == 65 | \/ ClientConnect 66 | \/ ClientConnected 67 | \/ ClientClose 68 | \/ ClientClosed 69 | 70 | ----------------------------------------------------------------------------------------- 71 | 72 | ServerConnect == 73 | /\ svr = closed 74 | /\ cli = connecting 75 | /\ svr' = connecting 76 | /\ UNCHANGED cli 77 | 78 | ServerConnected == 79 | /\ svr = connecting 80 | /\ cli \in {connecting,open} 81 | /\ svr' = open 82 | /\ UNCHANGED cli 83 | 84 | ServerClose == 85 | /\ svr /= closed 86 | /\ svr' = closing 87 | /\ UNCHANGED cli 88 | 89 | ServerClosed == 90 | /\ svr' = closed 91 | /\ UNCHANGED cli 92 | 93 | Server == 94 | \/ ServerConnect 95 | \/ ServerConnected 96 | \/ ServerClose 97 | \/ ServerClosed 98 | 99 | ----------------------------------------------------------------------------------------- 100 | 101 | Next == 102 | \/ Client 103 | \/ Server 104 | 105 | Spec == Init /\ [][Next]_vars 106 | 107 | ======================================================================================================================== 108 | -------------------------------------------------------------------------------- /bin/add_source_links_to_readme: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | cd "$(dirname "$0")/.." 4 | 5 | # cd "$(dirname "$(readlink -e "$0")")" || exit 1 6 | # [ $# -ne 1 ] && echo "Usage: $0 " && exit 1 7 | # tmp=/tmp/$(date +%Y%m%d-%H%M%S)-$$ 8 | 9 | src=README.md 10 | tmp=/tmp/webapp-util-README.tmp.md 11 | 12 | # TODO: In some cases it chooses a /jvm/ link for something declared as shared 13 | 14 | function process { 15 | local name= 16 | local file= 17 | local tail= 18 | 19 | while IFS= read -r line; do 20 | 21 | name="$(echo "$line" | perl -pe 's/^ \* `(.+?)`.*/$1/')" 22 | 23 | if [[ "$name" == "$line" ]]; then 24 | echo "$line" 25 | else 26 | file="$(find core* test* db* -name "$name.scala" | fgrep /src/ | head -1 || echo)" 27 | if [ -z "$file" ]; then 28 | echo >&2 "Failed to find the source for $name" 29 | echo "$line" 30 | else 31 | tail="$(echo "$line" | perl -pe 's/^.*?`.+?`(.*)/$1/')" 32 | echo " * [\`$name\`](./$file)$tail" 33 | fi 34 | fi 35 | 36 | done < $src 37 | } 38 | 39 | # Dry run 40 | # process | bat -l markdown --color=always --pager=never - 41 | 42 | process > $tmp 43 | cp $src /tmp/webapp-util-$src 44 | mv $tmp $src 45 | echo Done -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / homepage := Some(url("https://github.com/japgolly/webapp-util")) 2 | ThisBuild / licenses := ("Apache-2.0", url("http://opensource.org/licenses/Apache-2.0")) :: Nil 3 | ThisBuild / organization := "com.github.japgolly.webapp-util" 4 | ThisBuild / shellPrompt := ((s: State) => Project.extract(s).currentRef.project + "> ") 5 | ThisBuild / startYear := Some(2021) 6 | ThisBuild / versionScheme := Some("early-semver") 7 | sonatypeProfileName := "com.github.japgolly" 8 | 9 | val root = Build.root 10 | 11 | val coreJS = Build.coreJS 12 | val coreJVM = Build.coreJVM 13 | val testCoreJS = Build.testCoreJS 14 | val testCoreJVM = Build.testCoreJVM 15 | 16 | val testNode = Build.testNode 17 | 18 | val coreBoopickleJS = Build.coreBoopickleJS 19 | val coreBoopickleJVM = Build.coreBoopickleJVM 20 | val testBoopickleJS = Build.testBoopickleJS 21 | val testBoopickleJVM = Build.testBoopickleJVM 22 | 23 | val coreCatsEffectJS = Build.coreCatsEffectJS 24 | val coreCatsEffectJVM = Build.coreCatsEffectJVM 25 | val testCatsEffectJS = Build.testCatsEffectJS 26 | val testCatsEffectJVM = Build.testCatsEffectJVM 27 | 28 | val coreCirceJS = Build.coreCirceJS 29 | val coreCirceJVM = Build.coreCirceJVM 30 | val testCirceJS = Build.testCirceJS 31 | val testCirceJVM = Build.testCirceJVM 32 | 33 | val coreOkHttp4 = Build.coreOkHttp4 34 | 35 | val dbPostgres = Build.dbPostgres 36 | val testDbPostgres = Build.testDbPostgres 37 | 38 | val examplesJVM = Build.examplesJVM 39 | val examplesJS = Build.examplesJS 40 | val ghpages = Build.ghpages 41 | -------------------------------------------------------------------------------- /core/js/src/main/scala-2/japgolly/webapputil/general/internal/MergeErrors.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general.internal 2 | 3 | import scala.annotation._ 4 | 5 | @implicitNotFound("Don't know how to merge Either[${E1}, ${E2}]") 6 | trait MergeErrors[-E1, -E2] { 7 | type E 8 | val merge: Either[E1, E2] => E 9 | } 10 | 11 | object MergeErrors extends MergeErrors3 { 12 | type To[-E1, -E2, EE] = MergeErrors[E1, E2] { type E = EE } 13 | 14 | def apply[A, B, C](f: Either[A, B] => C): To[A, B, C] = 15 | new MergeErrors[A, B] { 16 | override type E = C 17 | override val merge = f 18 | } 19 | 20 | @nowarn("msg=match may not be exhaustive") 21 | implicit def nothingRight[A]: MergeErrors.To[A, Nothing, A] = 22 | MergeErrors[A, Nothing, A] { case Left(a) => a } 23 | } 24 | 25 | trait MergeErrors3 extends MergeErrors2 { 26 | @nowarn("msg=match may not be exhaustive") 27 | implicit def nothingLeft[A]: MergeErrors.To[Nothing, A, A] = 28 | MergeErrors[Nothing, A, A] { case Right(a) => a } 29 | } 30 | 31 | trait MergeErrors2 extends MergeErrors1 { 32 | implicit def same[A, B](implicit ev: A =:= B): MergeErrors.To[A, B, B] = { 33 | MergeErrors(_.fold(ev, identity)) 34 | } 35 | } 36 | 37 | trait MergeErrors1 { 38 | implicit def disjoint[A, B]: MergeErrors.To[A, B, Either[A, B]] = 39 | MergeErrors(identity) 40 | } 41 | -------------------------------------------------------------------------------- /core/js/src/main/scala-3/japgolly/webapputil/general/internal/MergeErrors.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general.internal 2 | 3 | import scala.annotation._ 4 | import scala.compiletime._ 5 | 6 | @implicitNotFound("Don't know how to merge Either[${E1}, ${E2}]") 7 | trait MergeErrors[-E1, -E2] { 8 | type E 9 | val merge: Either[E1, E2] => E 10 | } 11 | 12 | object MergeErrors extends MergeErrors2 { 13 | type To[-E1, -E2, EE] = MergeErrors[E1, E2] { type E = EE } 14 | 15 | def apply[A, B, C](f: Either[A, B] => C): To[A, B, C] = 16 | new MergeErrors[A, B] { 17 | override type E = C 18 | override val merge = f 19 | } 20 | 21 | @nowarn("msg=match may not be exhaustive") 22 | implicit def nothingRight[A]: MergeErrors.To[A, Nothing, A] = 23 | MergeErrors[A, Nothing, A] { case Left(a) => a } 24 | } 25 | 26 | trait MergeErrors2 extends MergeErrors1 { 27 | @nowarn("msg=match may not be exhaustive") 28 | implicit def nothingLeft[A]: MergeErrors.To[Nothing, A, A] = 29 | MergeErrors[Nothing, A, A] { case Right(a) => a } 30 | } 31 | 32 | trait MergeErrors1 { 33 | @nowarn("msg=match may not be exhaustive") 34 | transparent inline implicit def derive[A, B]: MergeErrors[A, B] = 35 | summonFrom { 36 | case ev: (A =:= B) => MergeErrors[A, B, B](_.fold(ev, identity)) 37 | case _ => MergeErrors[A, B, Either[A, B]](identity) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/ajax/AjaxException.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.ajax 2 | 3 | import org.scalajs.dom.XMLHttpRequest 4 | 5 | /** 6 | * Thrown when `Ajax.get` or `Ajax.post` receives a non-20X response code. 7 | * Contains the XMLHttpRequest that resulted in that response 8 | * 9 | * This used to be in scalajs-dom but was deprecated in v2.0.0. 10 | */ 11 | case class AjaxException(xhr: XMLHttpRequest) extends Exception { 12 | def isTimeout = xhr.status == 0 && xhr.readyState == 4 13 | } 14 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/binary/BinaryData_PlatformSpecific.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.binary 2 | 3 | // ********** 4 | // * * 5 | // * JS * 6 | // * * 7 | // ********** 8 | 9 | import org.scalajs.dom.Blob 10 | import scala.scalajs.js 11 | import scala.scalajs.js.JSConverters._ 12 | import scala.scalajs.js.typedarray.{ArrayBuffer, Uint8Array} 13 | 14 | trait BinaryData_PlatformSpecific_Object { self: BinaryData.type => 15 | 16 | def fromArrayBuffer(ab: ArrayBuffer): BinaryData = 17 | BinaryData.fromByteBuffer(BinaryJs.arrayBufferToByteBuffer(ab)) 18 | 19 | def fromUint8Array(a: Uint8Array): BinaryData = 20 | fromArrayBuffer(BinaryJs.uint8ArrayToArrayBuffer(a)) 21 | 22 | def unsafeFromArrayBuffer(ab: ArrayBuffer): BinaryData = 23 | BinaryData.unsafeFromByteBuffer(BinaryJs.arrayBufferToByteBuffer(ab)) 24 | 25 | def unsafeFromUint8Array(a: Uint8Array): BinaryData = 26 | unsafeFromArrayBuffer(BinaryJs.uint8ArrayToArrayBuffer(a)) 27 | } 28 | 29 | trait BinaryData_PlatformSpecific_Instance { self: BinaryData => 30 | 31 | def toArrayBuffer: ArrayBuffer = 32 | BinaryJs.byteBufferToArrayBuffer(self.unsafeByteBuffer) 33 | 34 | def toUint8Array: Uint8Array = 35 | new Uint8Array(toArrayBuffer) 36 | 37 | def toBlob: Blob = 38 | BinaryJs.byteBufferToBlob(self.unsafeByteBuffer) 39 | 40 | def toNewJsArray: js.Array[Byte] = 41 | self.toNewArray.toJSArray 42 | 43 | def unsafeArrayBuffer: ArrayBuffer = 44 | BinaryJs.byteBufferToArrayBuffer(self.unsafeByteBuffer) 45 | 46 | def unsafeUint8Array: Uint8Array = 47 | new Uint8Array(unsafeArrayBuffer) 48 | 49 | def unsafeBlob: Blob = 50 | BinaryJs.byteBufferToBlob(self.unsafeByteBuffer) 51 | 52 | def unsafeJsArray: js.Array[Byte] = 53 | self.unsafeArray.toJSArray 54 | } 55 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/binary/BinaryFormat.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.binary 2 | 3 | import japgolly.scalajs.react.AsyncCallback 4 | 5 | /** A means of converting instances of type `A` to a binary format and back. */ 6 | final class BinaryFormat[A](val encode: A => AsyncCallback[BinaryData], 7 | val decode: BinaryData => AsyncCallback[A]) { 8 | 9 | def xmap[B](onDecode: A => B)(onEncode: B => A): BinaryFormat[B] = 10 | // Delegating because decoding can fail and must be wrapped to be pure 11 | xmapAsync( 12 | a => AsyncCallback.delay(onDecode(a)))( 13 | b => AsyncCallback.delay(onEncode(b))) 14 | 15 | def xmapAsync[B](onDecode: A => AsyncCallback[B])(onEncode: B => AsyncCallback[A]): BinaryFormat[B] = 16 | BinaryFormat.async( 17 | decode(_).flatMap(onDecode))( 18 | onEncode(_).flatMap(encode)) 19 | 20 | type ThisIsBinary = BinaryFormat[A] =:= BinaryFormat[BinaryData] 21 | 22 | def encrypt(e: Encryption)(implicit ev: ThisIsBinary): BinaryFormat[BinaryData] = 23 | ev(this).xmapAsync(e.decrypt)(e.encrypt) 24 | 25 | def compress(c: Compression)(implicit ev: ThisIsBinary): BinaryFormat[BinaryData] = 26 | ev(this).xmap(c.decompressOrThrow)(c.compress) 27 | } 28 | 29 | object BinaryFormat { 30 | 31 | val id: BinaryFormat[BinaryData] = { 32 | val f: BinaryData => AsyncCallback[BinaryData] = AsyncCallback.pure 33 | async(f)(f) 34 | } 35 | 36 | def apply[A](decode: BinaryData => A) 37 | (encode: A => BinaryData): BinaryFormat[A] = 38 | async( 39 | b => AsyncCallback.delay(decode(b)))( 40 | a => AsyncCallback.delay(encode(a))) 41 | 42 | def async[A](decode: BinaryData => AsyncCallback[A]) 43 | (encode: A => AsyncCallback[BinaryData]): BinaryFormat[A] = 44 | new BinaryFormat(encode, decode) 45 | } 46 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/binary/BinaryJs.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.binary 2 | 3 | import java.nio.ByteBuffer 4 | import org.scalajs.dom.{Blob, FileReader, window} 5 | import scala.scalajs.js 6 | import scala.scalajs.js.JSConverters._ 7 | import scala.scalajs.js.typedarray.TypedArrayBufferOps._ 8 | import scala.scalajs.js.typedarray._ 9 | 10 | object BinaryJs extends BinaryJs 11 | 12 | trait BinaryJs { 13 | 14 | final def arrayBufferToBlob(a: ArrayBuffer): Blob = 15 | new Blob(js.Array(a)) 16 | 17 | @inline final def arrayBufferToByteBuffer(a: ArrayBuffer): ByteBuffer = 18 | TypedArrayBuffer.wrap(a) 19 | 20 | final def base64ToByteBuffer(base64: String): ByteBuffer = { 21 | val binstr = window.atob(base64) 22 | val buf = new Int8Array(binstr.length) 23 | var i = 0 24 | binstr.foreach { ch => 25 | buf(i) = ch.toByte 26 | i += 1 27 | } 28 | TypedArrayBuffer.wrap(buf) 29 | } 30 | 31 | final def blobToArrayBuffer(blob: Blob): ArrayBuffer = { 32 | var arrayBuffer: ArrayBuffer = null 33 | val fileReader = new FileReader() 34 | fileReader.onload = e => arrayBuffer = e.target.asInstanceOf[js.Dynamic].result.asInstanceOf[ArrayBuffer] 35 | fileReader.readAsArrayBuffer(blob) 36 | assert(arrayBuffer != null) 37 | arrayBuffer 38 | } 39 | 40 | final def byteBufferToArrayBuffer(bb: ByteBuffer): ArrayBuffer = 41 | int8ArrayToArrayBuffer(byteBufferToInt8Array(bb)) 42 | 43 | final def byteBufferToBlob(bb: ByteBuffer): Blob = 44 | arrayBufferToBlob(byteBufferToArrayBuffer(bb)) 45 | 46 | final def byteBufferToInt8Array(bb: ByteBuffer): Int8Array = { 47 | val limit = bb.limit() 48 | if (bb.hasTypedArray()) 49 | bb.typedArray() 50 | else if (bb.hasArray) { 51 | var array = bb.array() 52 | val offset = bb.arrayOffset() 53 | if (limit != array.length) 54 | array = array.slice(offset, offset + limit) 55 | new Int8Array(array.toJSArray) 56 | } else { 57 | val array = BinaryData.unsafeFromByteBuffer(bb).unsafeJsArray 58 | new Int8Array(array) 59 | } 60 | } 61 | 62 | final def int8ArrayToArrayBuffer(v: Int8Array): ArrayBuffer = 63 | arrayBufferViewToArrayBuffer(v) 64 | 65 | final def uint8ArrayToArrayBuffer(v: Uint8Array): ArrayBuffer = 66 | arrayBufferViewToArrayBuffer(v) 67 | 68 | final private def arrayBufferViewToArrayBuffer(v: ArrayBufferView): ArrayBuffer = { 69 | val off = v.byteOffset 70 | val len = v.byteLength 71 | if (len == v.buffer.byteLength) 72 | v.buffer 73 | else 74 | v.buffer.slice(off, off + len) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/binary/BinaryString.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.binary 2 | 3 | import scala.scalajs.js 4 | import scala.scalajs.js.typedarray.Uint8Array 5 | 6 | /** Binary data efficiently encoded as a UTF-16 string. */ 7 | final class BinaryString(val encoded: String)(implicit enc: BinaryString.Encoder) { 8 | 9 | lazy val binaryValue: BinaryData = 10 | BinaryData.unsafeFromUint8Array(enc.decode(encoded)) 11 | } 12 | 13 | object BinaryString { 14 | 15 | def encoded(str: String)(implicit enc: Encoder): BinaryString = 16 | new BinaryString(str) 17 | 18 | def apply(bin: BinaryData)(implicit enc: Encoder): BinaryString = 19 | encoded(enc.encode(bin.unsafeUint8Array)) 20 | 21 | /** It is recommended that you use the JS `base32768` library to back this. */ 22 | trait Encoder { 23 | def encode(bin: Uint8Array): String 24 | def decode(str: String): Uint8Array 25 | } 26 | 27 | // =================================================================================================================== 28 | 29 | object Base32768 { 30 | 31 | def global: Encoder = 32 | apply(js.Dynamic.global.base32768) 33 | 34 | def apply(jsInstance: Any): Encoder = { 35 | assert(js.typeOf(jsInstance) == "object", "JS object expected. Got: " + jsInstance) 36 | val d = jsInstance.asInstanceOf[js.Dynamic] 37 | assert(js.typeOf(d.encode) == "function", ".encode is not a function") 38 | assert(js.typeOf(d.decode) == "function", ".decode is not a function") 39 | force(jsInstance) 40 | } 41 | 42 | def force(jsInstance: Any): Encoder = { 43 | val d = jsInstance.asInstanceOf[js.Dynamic] 44 | new Encoder { 45 | override def decode(str: String): Uint8Array = d.decode(str).asInstanceOf[Uint8Array] 46 | override def encode(bin: Uint8Array): String = d.encode(bin).asInstanceOf[String] 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/binary/Compression.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.binary 2 | 3 | import scala.scalajs.js 4 | import scala.scalajs.js.typedarray.Uint8Array 5 | import scala.util.Try 6 | 7 | /** A means of binary compression and decompression. */ 8 | final case class Compression(compress : BinaryData => BinaryData, 9 | decompress: BinaryData => Try[BinaryData]) { 10 | 11 | def decompressOrThrow: BinaryData => BinaryData = 12 | decompress(_).get 13 | } 14 | 15 | object Compression { 16 | 17 | object ViaPako { 18 | 19 | /** @param level Compression level [1-9] 20 | * @param addHeaders Add header and adler32 crc 21 | */ 22 | def apply(level: Int, addHeaders: Boolean)(implicit pako: Pako): Compression = { 23 | val deflateOptions = js.Dynamic.literal().asInstanceOf[Pako.DeflateOptions] 24 | deflateOptions.level = level 25 | if (addHeaders) 26 | Compression( 27 | compress = data => pako.deflate(data, deflateOptions), 28 | decompress = data => Try(pako.inflate(data)), 29 | ) 30 | else 31 | Compression( 32 | compress = data => pako.deflateRaw(data, deflateOptions), 33 | decompress = data => Try(pako.inflateRaw(data)), 34 | ) 35 | } 36 | 37 | def maxWithoutHeaders(implicit pako: Pako): Compression = 38 | apply(level = 9, addHeaders = false) 39 | 40 | def maxWithHeaders(implicit pako: Pako): Compression = 41 | apply(level = 9, addHeaders = true) 42 | 43 | private implicit def binaryDataToPakoData(b: BinaryData): Pako.Data = 44 | b.unsafeUint8Array 45 | 46 | private implicit def pakoDataToBinaryData(d: Pako.Data): BinaryData = 47 | BinaryData.unsafeFromUint8Array(d.asInstanceOf[Uint8Array]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/binary/Encryption.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.binary 2 | 3 | import japgolly.scalajs.react.AsyncCallback 4 | 5 | /** A means of binary encryption and decryption. */ 6 | final case class Encryption(encrypt: BinaryData => AsyncCallback[BinaryData], 7 | decrypt: BinaryData => AsyncCallback[BinaryData]) 8 | 9 | object Encryption { 10 | 11 | trait Engine { 12 | def apply(symmetricKey: BinaryData): AsyncCallback[Encryption] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/binary/Pako.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.binary 2 | 3 | import scala.annotation.nowarn 4 | import scala.scalajs.js 5 | import scala.scalajs.js.typedarray.Uint8Array 6 | import scala.scalajs.js.| 7 | 8 | /** Facade for the JS `pako` library with provides zlib compression & decompression. */ 9 | @js.native 10 | @nowarn("msg=dead|never used") 11 | sealed trait Pako extends js.Object { 12 | import Pako._ 13 | 14 | def deflate(data: Data, options: DeflateOptions = js.native): Data = js.native 15 | 16 | /** The same as deflate, but creates raw data, without wrapper (header and adler32 crc). */ 17 | def deflateRaw(data: Data, options: DeflateOptions = js.native): Data = js.native 18 | 19 | /** Throws an exception on error */ 20 | def inflate(data: Data, options: InflateOptions = js.native): Data = js.native 21 | 22 | /** The same as inflate, but creates raw data, without wrapper (header and adler32 crc). 23 | * 24 | * Throws an exception on error. 25 | */ 26 | def inflateRaw(data: Data, options: InflateOptions = js.native): Data = js.native 27 | } 28 | 29 | @nowarn("msg=dead|never used") 30 | object Pako { 31 | 32 | type Data = Uint8Array | js.Array[Int] | String 33 | 34 | /** See http://zlib.net/manual.html#Advanced */ 35 | @js.native 36 | trait DeflateOptions extends js.Object { 37 | /** Z_NO_COMPRESSION: 0 38 | * Z_BEST_SPEED: 1 39 | * Z_BEST_COMPRESSION: 9 40 | * Z_DEFAULT_COMPRESSION: -1 41 | */ 42 | var level : js.UndefOr[Int] = js.native 43 | var windowBits: js.UndefOr[Int] = js.native 44 | var memLevel : js.UndefOr[Int] = js.native 45 | var strategy : js.UndefOr[Int] = js.native 46 | var dictionary: js.UndefOr[Int] = js.native 47 | } 48 | 49 | 50 | @js.native 51 | trait InflateOptions extends js.Object { 52 | var windowBits: js.UndefOr[Int] = js.native 53 | } 54 | 55 | // =================================================================================================================== 56 | 57 | def global: Pako = 58 | apply(js.Dynamic.global.pako) 59 | 60 | def apply(jsInstance: Any): Pako = { 61 | assert(js.typeOf(jsInstance) == "object", "JS object expected. Got: " + jsInstance) 62 | val d = jsInstance.asInstanceOf[js.Dynamic] 63 | assert(js.typeOf(d.deflate) == "function", ".deflate is not a function") 64 | force(jsInstance) 65 | } 66 | 67 | def force(jsInstance: Any): Pako = 68 | jsInstance.asInstanceOf[Pako] 69 | } 70 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/browser/WindowConfirm.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.browser 2 | 3 | import japgolly.scalajs.react.{CallbackTo, Reusability} 4 | 5 | /** Abstraction over `window.confirm`. */ 6 | trait WindowConfirm { 7 | def apply(msg: String): CallbackTo[Boolean] 8 | } 9 | 10 | object WindowConfirm { 11 | 12 | val real: WindowConfirm = 13 | CallbackTo.confirm(_) 14 | 15 | def const(b: Boolean): WindowConfirm = 16 | const(CallbackTo.pure(b)) 17 | 18 | def const(cb: CallbackTo[Boolean]): WindowConfirm = 19 | _ => cb 20 | 21 | implicit def reusability: Reusability[WindowConfirm] = 22 | Reusability.byRef 23 | } -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/browser/WindowLocation.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.browser 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.webapputil.general.Url 5 | import org.scalajs.dom.window 6 | 7 | trait WindowLocation { 8 | def setHref (url: Url.Absolute): Callback 9 | def setHrefRelative(url: Url.Relative): Callback 10 | } 11 | 12 | object WindowLocation { 13 | 14 | object Real extends WindowLocation { 15 | private[this] def set(href: String) = Callback { 16 | window.location.href = href 17 | } 18 | 19 | override def setHref (url: Url.Absolute) = set(url.absoluteUrl) 20 | override def setHrefRelative(url: Url.Relative) = set(url.relativeUrl) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/browser/WindowPrompt.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.browser 2 | 3 | import japgolly.scalajs.react.{CallbackTo, Reusability} 4 | 5 | /** Abstraction over `window.prompt`. */ 6 | trait WindowPrompt { 7 | def apply(message: String): CallbackTo[Option[String]] 8 | def apply(message: String, default: String): CallbackTo[Option[String]] 9 | } 10 | 11 | object WindowPrompt { 12 | 13 | val real: WindowPrompt = 14 | new WindowPrompt { 15 | override def apply(message: String): CallbackTo[Option[String]] = 16 | CallbackTo.prompt(message) 17 | 18 | override def apply(message: String, default: String): CallbackTo[Option[String]] = 19 | CallbackTo.prompt(message, default) 20 | } 21 | 22 | def const(answer: Option[String]): WindowPrompt = 23 | const(CallbackTo.pure(answer)) 24 | 25 | def const(cb: CallbackTo[Option[String]]): WindowPrompt = 26 | new WindowPrompt { 27 | override def apply(message: String) = cb 28 | override def apply(message: String, default: String) = cb 29 | } 30 | 31 | implicit def reusability: Reusability[WindowPrompt] = 32 | Reusability.byRef 33 | } -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/entrypoint/Entrypoint.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.entrypoint 2 | 3 | import org.scalajs.dom 4 | import scala.scalajs.js.annotation.JSExport 5 | 6 | abstract class Entrypoint[Input](final val defn: EntrypointDef[Input]) { 7 | 8 | @JSExport(EntrypointDef.MainMethodName) 9 | final def main(encodedInput: String): Unit = 10 | run(decodeInput(encodedInput)) 11 | 12 | final def decodeInput(s: String): Input = 13 | defn.codec.decodeOrThrow(s) 14 | 15 | @inline final protected def `#root` = dom.document.getElementById("root") 16 | @inline final protected def `#main` = dom.document.getElementById("main") 17 | 18 | def run(i: Input): Unit 19 | } 20 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/general/CallbackHelpers.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.scalajs.react._ 4 | import scala.util.Try 5 | 6 | private[webapputil] object CallbackHelpers { 7 | 8 | @inline final implicit class EitherExt[A, B](private val self: Either[A, B]) extends AnyVal { 9 | def leftMap[C](f: A => C): Either[C, B] = 10 | self match { 11 | case r@ Right(_) => r.widen 12 | case Left(a) => Left(f(a)) 13 | } 14 | } 15 | 16 | @inline final implicit class RightExt[A](private val self: Right[Any, A]) extends AnyVal { 17 | @inline def widen: Right[Nothing, A] = self.asInstanceOf[Right[Nothing, A]] 18 | } 19 | 20 | @inline final implicit class LeftExt[A](private val self: Left[A, Any]) extends AnyVal { 21 | @inline def widen: Left[A, Nothing] = self.asInstanceOf[Left[A, Nothing]] 22 | } 23 | 24 | final class HelperAsyncCallbackDisj[E, A](private val underlying: (Try[Either[E, A]] => Callback) => Callback) extends AnyVal { 25 | @inline private def self: AsyncCallback[Either[E, A]] = 26 | AsyncCallback(underlying) 27 | 28 | def leftFlatFlatMap[F](f: E => AsyncCallback[Either[F, A]]): AsyncCallback[Either[F, A]] = 29 | self.flatMap { 30 | case r@ Right(_) => AsyncCallback.pure(r.widen) 31 | case Left(e) => f(e) 32 | } 33 | 34 | def leftFlatMap[F](f: E => AsyncCallback[F]): AsyncCallback[Either[F, A]] = 35 | leftFlatFlatMap(f.andThen(_.map(Left(_)))) 36 | 37 | // def leftFlatTap[F](f: E => AsyncCallback[F]): AsyncCallback[Either[E, A]] = 38 | // self.flatMap { 39 | // case l@ Left(e) => f(e).ret(l) 40 | // case r@ Right(_) => AsyncCallback.pure(r) 41 | // } 42 | 43 | // def leftFlatTapSync[F](f: E => CallbackTo[F]): AsyncCallback[Either[E, A]] = 44 | // leftFlatTap(f.andThen(_.asAsyncCallback)) 45 | 46 | def rightFlatFlatMap[B](f: A => AsyncCallback[Either[E, B]]): AsyncCallback[Either[E, B]] = 47 | self.flatMap { 48 | case Right(a) => f(a) 49 | case l@ Left(_) => AsyncCallback.pure(l.widen) 50 | } 51 | 52 | def rightFlatMap[B](f: A => AsyncCallback[B]): AsyncCallback[Either[E, B]] = 53 | rightFlatFlatMap(f.andThen(_.map(Right(_)))) 54 | 55 | // def rightFlatTap[B](f: A => AsyncCallback[B]): AsyncCallback[Either[E, A]] = 56 | // self.flatMap { 57 | // case r@ Right(a) => f(a).ret(r) 58 | // case l@ Left(_) => AsyncCallback.pure(l) 59 | // } 60 | 61 | // def rightFlatTapSync[B](f: A => CallbackTo[B]): AsyncCallback[Either[E, A]] = 62 | // rightFlatTap(f.andThen(_.asAsyncCallback)) 63 | } 64 | 65 | implicit def HelperAsyncCallbackDisj[E, A](a: AsyncCallback[Either[E, A]]): HelperAsyncCallbackDisj[E, A] = 66 | new HelperAsyncCallbackDisj(a.completeWith) 67 | 68 | } 69 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/general/Effect_PlatformSpecific.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.scalajs.react.callback._ 4 | import scala.util.Try 5 | 6 | object Effect_PlatformSpecific { 7 | 8 | object callback extends Effect.Sync[CallbackTo] { 9 | 10 | @inline override def delay[A](a: => A) = 11 | CallbackTo(a) 12 | 13 | @inline override def pure[A](a: A) = 14 | CallbackTo.pure(a) 15 | 16 | @inline override def map[A, B](fa: CallbackTo[A])(f: A => B) = 17 | fa.map(f) 18 | 19 | @inline override def flatMap[A, B](fa: CallbackTo[A])(f: A => CallbackTo[B]) = 20 | fa.flatMap(f) 21 | 22 | override def bracket[A, B](fa: CallbackTo[A])(use: A => CallbackTo[B])(release: A => CallbackTo[Unit]): CallbackTo[B] = 23 | fa.flatMap(a => use(a).finallyRun(release(a))) 24 | 25 | override def runSync[A](fa: CallbackTo[A]): A = 26 | fa.runNow() 27 | } 28 | 29 | object asyncCallback extends Effect.Async[AsyncCallback] { 30 | 31 | @inline override def delay[A](a: => A) = 32 | AsyncCallback.delay(a) 33 | 34 | @inline override def pure[A](a: A) = 35 | AsyncCallback.pure(a) 36 | 37 | @inline override def map[A, B](fa: AsyncCallback[A])(f: A => B) = 38 | fa.map(f) 39 | 40 | @inline override def flatMap[A, B](fa: AsyncCallback[A])(f: A => AsyncCallback[B]) = 41 | fa.flatMap(f) 42 | 43 | override def timeoutMs[A](ms: Long)(fa: AsyncCallback[A]): AsyncCallback[Option[A]] = 44 | fa.timeoutMs(ms.toDouble) 45 | 46 | override def bracket[A, B](fa: AsyncCallback[A])(use: A => AsyncCallback[B])(release: A => AsyncCallback[Unit]): AsyncCallback[B] = 47 | fa.flatMap(a => use(a).finallyRun(release(a))) 48 | 49 | override def async[A](fa: (Try[A] => Unit) => Unit): AsyncCallback[A] = 50 | AsyncCallback[A](f => CallbackTo(fa(f(_).runNow()))) 51 | } 52 | 53 | } 54 | 55 | trait Effect_PlatformSpecific { 56 | implicit def callback: Effect.Sync[CallbackTo] = Effect_PlatformSpecific.callback 57 | implicit def asyncCallback: Effect.Async[AsyncCallback] = Effect_PlatformSpecific.asyncCallback 58 | } 59 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/general/JsExt.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import scala.scalajs.js 4 | 5 | object JsExt { 6 | 7 | @inline final implicit class JsAnyExt(private val self: Any) extends AnyVal { 8 | @inline def falsy: Boolean = { 9 | val a = self.asInstanceOf[js.Dynamic] 10 | (!a).asInstanceOf[Boolean] 11 | } 12 | 13 | @inline def truthy: Boolean = 14 | !falsy 15 | } 16 | 17 | @inline final implicit class JsArrayExt[A](private val self: js.Array[A]) extends AnyVal { 18 | def forEachJs(f: js.Function1[A, Unit]): Unit = { 19 | self.asInstanceOf[js.Dynamic].forEach(f) 20 | () 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/general/TimersJs.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import scala.scalajs.js.timers.{SetIntervalHandle, SetTimeoutHandle} 4 | 5 | trait TimersJs { 6 | 7 | def setTimeout(interval: Double)(body: => Unit): SetTimeoutHandle 8 | 9 | def clearTimeout(handle: SetTimeoutHandle): Unit 10 | 11 | def setInterval(interval: Double)(body: => Unit): SetIntervalHandle 12 | 13 | def clearInterval(handle: SetIntervalHandle): Unit 14 | } 15 | 16 | object TimersJs { 17 | 18 | val real: TimersJs = 19 | new TimersJs { 20 | import scala.scalajs.js.timers.RawTimers 21 | 22 | def setTimeout(interval: Double)(body: => Unit) = 23 | RawTimers.setTimeout(() => body, interval) 24 | 25 | override def clearTimeout(handle: SetTimeoutHandle) = 26 | RawTimers.clearTimeout(handle) 27 | 28 | def setInterval(interval: Double)(body: => Unit) = 29 | RawTimers.setInterval(() => body, interval) 30 | 31 | override def clearInterval(handle: SetIntervalHandle) = 32 | RawTimers.clearInterval(handle) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/general/VarJs.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.scalajs.react.callback._ 4 | import japgolly.univeq._ 5 | import japgolly.webapputil.webstorage.AbstractWebStorage 6 | import scala.scalajs.js 7 | 8 | /** Immutable reference to a potentially abstract, potentially mutable variable. */ 9 | final class VarJs[A](val unsafeGet: () => A, val unsafeSet: A => Unit) { self => 10 | 11 | def xmap[B](f: A => B)(g: B => A): VarJs[B] = 12 | VarJs.viaFns(f(self.unsafeGet()))(b => self.unsafeSet(g(b))) 13 | 14 | def get: CallbackTo[A] = 15 | CallbackTo(unsafeGet()) 16 | 17 | def set(a: A): Callback = 18 | Callback(unsafeSet(a)) 19 | } 20 | 21 | object VarJs { 22 | 23 | def apply[A](initialState: A): VarJs[A] = { 24 | var a = initialState 25 | viaFns(a)(a2 => a = a2) 26 | } 27 | 28 | def const[A](a: A): VarJs[A] = 29 | new VarJs(() => a, _ => ()) 30 | 31 | def unsafeField[A](j: Any, field: String): VarJs[A] = { 32 | val d = j.asInstanceOf[js.Dynamic] 33 | new VarJs( 34 | () => d.selectDynamic(field).asInstanceOf[A], 35 | a => d.updateDynamic(field)(a.asInstanceOf[js.Any])) 36 | } 37 | 38 | def viaFns[A](get: => A)(set: A => Unit): VarJs[A] = 39 | new VarJs(() => get, set) 40 | 41 | def viaCallbacks[A](get: CallbackTo[A])(set: A => Callback): VarJs[A] = 42 | viaFns(get.runNow())(set(_).runNow()) 43 | 44 | object webStorage { 45 | import AbstractWebStorage.{Key, Value} 46 | 47 | def apply(ws: AbstractWebStorage, key: Key): VarJs[Option[Value]] = 48 | viaFns(ws.getItem(key).runNow())(ws.setOrRemoveItem(key, _).runNow()) 49 | 50 | def withDefault(ws: AbstractWebStorage, key: Key, default: Value): VarJs[Value] = 51 | // apply(ws, key).xmap(_.getOrElse(default))(v => Option.unless(v ==* default)(v)) 52 | apply(ws, key).xmap(_.getOrElse(default))(Some(_)) 53 | 54 | def boolean(ws: AbstractWebStorage, key: Key, valueWhenEmpty: Boolean = false): VarJs[Boolean] = { 55 | val asValue = (b: Boolean) => Value(if (b) "1" else "0") 56 | withDefault(ws, key, asValue(valueWhenEmpty)).xmap(_.value ==* "1")(asValue) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/http/UrlEncoder.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.http 2 | 3 | import scala.scalajs.js.URIUtils 4 | 5 | object UrlEncoder extends UrlEncoderApi { 6 | 7 | override def encode(s: String): String = 8 | URIUtils.encodeURIComponent(s) 9 | .replace("%20", "+") // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#description 10 | 11 | override def decode(s: String): String = 12 | URIUtils.decodeURIComponent( 13 | s.replace('+', ' ') // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/indexeddb/IndexedDbKey.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.indexeddb 2 | 3 | import org.scalajs.dom.IDBKey 4 | import scala.scalajs.js.| 5 | 6 | final class IndexedDbKey private(val asJs: IDBKey) extends AnyVal { 7 | @inline def value = asJs.asInstanceOf[IndexedDbKey.Typed] 8 | } 9 | 10 | object IndexedDbKey { 11 | 12 | // https://w3c.github.io/IndexedDB/#key-construct 13 | // A key has an associated type which is one of: number, date, string, binary, or array. 14 | 15 | type Typed = String | Double 16 | 17 | @inline def apply(t: Typed): IndexedDbKey = 18 | fromJs(t) 19 | 20 | def fromJs(k: IDBKey): IndexedDbKey = 21 | new IndexedDbKey(k) 22 | } -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/indexeddb/KeyCodec.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.indexeddb 2 | 3 | import japgolly.scalajs.react.CallbackTo 4 | import java.util.UUID 5 | import scala.reflect.ClassTag 6 | import scala.scalajs.js 7 | 8 | final case class KeyCodec[A](encode: A => IndexedDbKey, 9 | decode: IndexedDbKey => CallbackTo[A]) { 10 | 11 | def xmap[B](onDecode: A => B)(onEncode: B => A): KeyCodec[B] = 12 | // Delegating because decoding can fail and must be wrapped to be pure 13 | xmapSync( 14 | a => CallbackTo(onDecode(a)))( 15 | onEncode) 16 | 17 | def xmapSync[B](onDecode: A => CallbackTo[B])(onEncode: B => A): KeyCodec[B] = 18 | KeyCodec[B]( 19 | encode = encode compose onEncode, 20 | decode = decode(_).flatMap(onDecode)) 21 | } 22 | 23 | object KeyCodec { 24 | 25 | lazy val double: KeyCodec[Double] = 26 | primative("Double") 27 | 28 | lazy val int: KeyCodec[Int] = 29 | primative("Int") 30 | 31 | lazy val long: KeyCodec[Long] = 32 | string.xmap(_.toLong)(_.toString) 33 | 34 | def primative[A](name: String)(implicit ev: A => IndexedDbKey.Typed, ct: ClassTag[A]): KeyCodec[A] = 35 | apply[A](a => IndexedDbKey(ev(a)), k => CallbackTo( 36 | (k.value: Any) match { 37 | case a: A => a 38 | case x => throw new js.JavaScriptException(s"Invalid IDB key found. $name expected, got: $x") 39 | } 40 | )) 41 | 42 | lazy val string: KeyCodec[String] = 43 | primative("String") 44 | 45 | lazy val uuid: KeyCodec[UUID] = 46 | string.xmap(UUID.fromString)(_.toString) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/indexeddb/ObjectStoreDef.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.indexeddb 2 | 3 | import japgolly.scalajs.react.{AsyncCallback, CallbackTo} 4 | import japgolly.univeq.UnivEq 5 | import org.scalajs.dom.IDBValue 6 | 7 | sealed trait ObjectStoreDef[K, V] { 8 | val name: String 9 | val keyCodec: KeyCodec[K] 10 | 11 | final type Key = K 12 | 13 | def sync: ObjectStoreDef.Sync[K, _] 14 | } 15 | 16 | object ObjectStoreDef { 17 | 18 | final case class Sync[K, V](name : String, 19 | keyCodec : KeyCodec[K], 20 | valueCodec: ValueCodec[V]) extends ObjectStoreDef[K, V] { 21 | 22 | type Value = V 23 | 24 | override def sync: this.type = 25 | this 26 | } 27 | 28 | // =================================================================================================================== 29 | 30 | final case class Async[K, V](name : String, 31 | keyCodec : KeyCodec[K], 32 | valueCodec: ValueCodec.Async[V]) extends ObjectStoreDef[K, V] { self => 33 | 34 | type Value = Async.Value { 35 | type KeyType = K 36 | type ValueType = V 37 | val store: self.type 38 | } 39 | 40 | def encode(v: V): AsyncCallback[Value] = 41 | valueCodec.encode(v).map(value) 42 | 43 | def value(v: IDBValue): Value = 44 | new Async.Value { 45 | override type KeyType = K 46 | override type ValueType = V 47 | override val store: self.type = self 48 | override val value = v 49 | } 50 | 51 | override val sync: Sync[K, Value] = { 52 | val syncValueCodec = ValueCodec[Value]( 53 | encode = v => CallbackTo.pure(v.value), 54 | decode = v => CallbackTo.pure(value(v)), 55 | ) 56 | Sync(name, keyCodec, syncValueCodec) 57 | } 58 | } 59 | 60 | object Async { 61 | 62 | sealed trait Value { 63 | type KeyType 64 | type ValueType 65 | val store: Async[KeyType, ValueType] 66 | val value: IDBValue 67 | 68 | final def decode: AsyncCallback[ValueType] = 69 | store.valueCodec.decode(value) 70 | 71 | final override def hashCode = 72 | store.name.hashCode * 29 + value.## 73 | 74 | final override def equals(o: Any) = 75 | o match { 76 | case x: Value => store.name == x.store.name && value == x.value 77 | case _ => false 78 | } 79 | } 80 | 81 | object Value { 82 | implicit def univEq[V <: Value]: UnivEq[V] = UnivEq.force 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/indexeddb/Txn.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.indexeddb 2 | 3 | import cats.Monad 4 | import japgolly.webapputil.indexeddb.TxnMode._ 5 | 6 | /** Embedded language for safely working with(in) an IndexedDB transaction. 7 | * 8 | * This is necessary because whilst all the transaction methods are async, any other type of asynchronicity is not 9 | * supported and will result in IndexedDB automatically committing and closing the transaction, in which case, 10 | * further interaction with the transaction will result in a runtime error. 11 | * 12 | * Therefore, returning [[AsyncCallback]] from within transactions is dangerous because it allows composition of 13 | * both kinds of asynchronicity. To avoid this, we use this embedded language and don't publicly expose its 14 | * interpretation/translation to [[AsyncCallback]]. From the call-site's point of view, a `Txn[A]` is completely 15 | * opaque. 16 | * 17 | * This also has a nice side-effect of ensuring that transaction completion is always awaited because we do it in the 18 | * transaction functions right after interpretation. Otherwise, the call-sites would always need to remember to do it 19 | * if live transaction access were exposed. 20 | * 21 | * @tparam A The return type. 22 | */ 23 | final case class Txn[+M <: TxnMode, +A](step: TxnStep[M, A]) { self => 24 | import TxnStep._ 25 | 26 | def map[B](f: A => B): Txn[M, B] = 27 | Txn(Map(step, f)) 28 | 29 | def void: Txn[M, Unit] = 30 | map(_ => ()) 31 | } 32 | 33 | object Txn { 34 | 35 | @inline implicit final class InvariantOps[M <: TxnMode, A](private val self: Txn[M, A]) extends AnyVal { 36 | import TxnStep._ 37 | 38 | def flatMap[N <: TxnMode, B](f: A => Txn[N, B])(implicit m: TxnMode.Merge[M, N]): Txn[m.Mode, B] = { 39 | val step = FlatMap[m.Mode, A, B](m.substM(self.step), a => m.substN(f(a).step)) 40 | Txn(step) 41 | } 42 | 43 | @inline def unless(cond: Boolean)(implicit ev: TxnStep[RO, Option[Nothing]] => Txn[M, Option[Nothing]]): Txn[M, Option[A]] = 44 | when(!cond) 45 | 46 | @inline def unless_(cond: Boolean)(implicit ev: TxnStep[RO, Unit] => Txn[M, Unit]): Txn[M, Unit] = 47 | when_(!cond) 48 | 49 | def when(cond: Boolean)(implicit ev: TxnStep[RO, Option[Nothing]] => Txn[M, Option[Nothing]]): Txn[M, Option[A]] = 50 | if (cond) self.map(Some(_)) else ev(none) 51 | 52 | def when_(cond: Boolean)(implicit ev: TxnStep[RO, Unit] => Txn[M, Unit]): Txn[M, Unit] = 53 | if (cond) self.void else ev(unit) 54 | 55 | def >>[N <: TxnMode, B](f: Txn[N, B])(implicit m: TxnMode.Merge[M, N]): Txn[m.Mode, B] = { 56 | val next = m.substN(f.step) 57 | val step = FlatMap[m.Mode, A, B](m.substM(self.step), _ => next) 58 | Txn(step) 59 | } 60 | } 61 | 62 | type CatsInstance[M <: TxnMode] = Monad[Txn[M, *]] 63 | 64 | def catsInstance[M <: TxnMode](dsl: TxnDsl[M]): CatsInstance[M] = 65 | new CatsInstance[M] { 66 | override def pure[A](a: A) = dsl.pure(a) 67 | override def map[A, B](fa: Txn[M, A])(f: A => B) = fa map f 68 | override def flatMap[A, B](fa: Txn[M, A])(f: A => Txn[M, B]) = fa flatMap f 69 | override def tailRecM[A, B](a: A)(f: A => Txn[M, Either[A, B]]) = dsl.tailRec(a)(f) 70 | } 71 | 72 | implicit def catsInstanceRO: CatsInstance[RO] = catsInstance(TxnDslRO) 73 | implicit def catsInstanceRW: CatsInstance[RW] = catsInstance(TxnDslRW) 74 | } 75 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/indexeddb/TxnMode.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.indexeddb 2 | 3 | sealed trait TxnMode 4 | 5 | object TxnMode { 6 | sealed trait RW extends TxnMode 7 | sealed trait RO extends RW 8 | 9 | // =================================================================================================================== 10 | 11 | trait Merge[M <: TxnMode, N <: TxnMode] { 12 | type Mode <: TxnMode 13 | 14 | def substM[F[+_ <: TxnMode, _], A](f: F[M, A]): F[Mode, A] 15 | def substN[F[+_ <: TxnMode, _], A](f: F[N, A]): F[Mode, A] 16 | } 17 | 18 | object Merge { 19 | type To[M <: TxnMode, N <: TxnMode, R <: TxnMode] = Merge[M, N] { type Mode = R } 20 | 21 | implicit def eql[M <: TxnMode]: To[M, M, M] = 22 | new Merge[M, M] { 23 | override type Mode = M 24 | override def substM[F[+_ <: TxnMode, _], A](f: F[M, A]) = f 25 | override def substN[F[+_ <: TxnMode, _], A](f: F[M, A]) = f 26 | } 27 | 28 | implicit def rorw: To[RO, RW, RW] = 29 | new Merge[RO, RW] { 30 | override type Mode = RW 31 | override def substM[F[+_ <: TxnMode, _], A](f: F[RO, A]) = f 32 | override def substN[F[+_ <: TxnMode, _], A](f: F[RW, A]) = f 33 | } 34 | 35 | implicit def rwro: To[RW, RO, RW] = 36 | new Merge[RW, RO] { 37 | override type Mode = RW 38 | override def substM[F[+_ <: TxnMode, _], A](f: F[RW, A]) = f 39 | override def substN[F[+_ <: TxnMode, _], A](f: F[RO, A]) = f 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/indexeddb/TxnStep.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.indexeddb 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.webapputil.indexeddb.IndexedDb.ObjectStore 5 | import org.scalajs.dom._ 6 | 7 | /** Embedded language for safely working with(in) an IndexedDB transaction. 8 | * 9 | * This is necessary because whilst all the transaction methods are async, any other type of asynchronicity is not 10 | * supported and will result in IndexedDB automatically committing and closing the transaction, in which case, 11 | * further interaction with the transaction will result in a runtime error. 12 | * 13 | * Therefore, returning [[AsyncCallback]] from within transactions is dangerous because it allows composition of 14 | * both kinds of asynchronicity. To avoid this, we use this embedded language and don't publicly expose its 15 | * interpretation/translation to [[AsyncCallback]]. From the call-site's point of view, a `Txn[A]` is completely 16 | * opaque. 17 | * 18 | * This also has a nice side-effect of ensuring that transaction completion is always awaited because we do it in the 19 | * transaction functions right after interpretation. Otherwise, the call-sites would always need to remember to do it 20 | * if live transaction access were exposed. 21 | * 22 | * @tparam A The return type. 23 | */ 24 | sealed trait TxnStep[+M <: TxnMode, +A] 25 | 26 | object TxnStep { 27 | import TxnMode._ 28 | 29 | final case class FlatMap [M <: TxnMode, A, B](from: TxnStep[M, A], f: A => TxnStep[M, B]) extends TxnStep[M, B] 30 | final case class Map [M <: TxnMode, A, B](from: TxnStep[M, A], f: A => B) extends TxnStep[M, B] 31 | final case class Suspend [M <: TxnMode, A] (body: CallbackTo[TxnStep[M, A]]) extends TxnStep[M, A] 32 | final case class TailRec [M <: TxnMode, A, B](a: A, f: A => TxnStep[M, Either[A, B]]) extends TxnStep[M, B] 33 | 34 | final case class Eval [A] (body: CallbackTo[A]) extends TxnStep[RO, A] 35 | final case class GetStore [K, V] (defn: ObjectStoreDef.Sync[K, V]) extends TxnStep[RO, ObjectStore[K, V]] 36 | final case class StoreGet [K, V] (store: ObjectStore[K, V], key: IndexedDbKey) extends TxnStep[RO, Option[V]] 37 | final case class StoreGetAllKeys[K, V] (store: ObjectStore[K, V]) extends TxnStep[RO, Vector[K]] 38 | final case class StoreGetAllVals[K, V] (store: ObjectStore[K, V]) extends TxnStep[RO, Vector[V]] 39 | 40 | final case class StoreAdd (store: ObjectStore[_, _], key: IndexedDbKey, value: IDBValue) extends TxnStep[RW, Unit] 41 | final case class StoreClear (store: ObjectStore[_, _]) extends TxnStep[RW, Unit] 42 | final case class StoreDelete [K, V] (store: ObjectStore[K, V], key: IndexedDbKey) extends TxnStep[RW, Unit] 43 | final case class StorePut (store: ObjectStore[_, _], key: IndexedDbKey, value: IDBValue) extends TxnStep[RW, Unit] 44 | 45 | val none: TxnStep[RO, Option[Nothing]] = 46 | pure(None) 47 | 48 | def pure[A](a: A): TxnStep[RO, A] = 49 | Eval(CallbackTo.pure(a)) 50 | 51 | val unit: TxnStep[RO, Unit] = 52 | Eval(Callback.empty) 53 | } 54 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/indexeddb/ValueCodec.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.indexeddb 2 | 3 | import japgolly.scalajs.react.{AsyncCallback, CallbackTo} 4 | import japgolly.webapputil.binary._ 5 | import java.util.UUID 6 | import org.scalajs.dom.IDBValue 7 | import scala.reflect.ClassTag 8 | import scala.scalajs.js 9 | import scala.scalajs.js.typedarray.ArrayBuffer 10 | 11 | final case class ValueCodec[A](encode: A => CallbackTo[IDBValue], 12 | decode: IDBValue => CallbackTo[A]) { 13 | 14 | def xmap[B](onDecode: A => B)(onEncode: B => A): ValueCodec[B] = 15 | // Delegating because decoding can fail and must be wrapped to be pure 16 | xmapSync( 17 | a => CallbackTo(onDecode(a)))( 18 | b => CallbackTo(onEncode(b))) 19 | 20 | def xmapSync[B](onDecode: A => CallbackTo[B])(onEncode: B => CallbackTo[A]): ValueCodec[B] = 21 | ValueCodec[B]( 22 | encode = onEncode(_).flatMap(encode), 23 | decode = decode(_).flatMap(onDecode)) 24 | 25 | def async: ValueCodec.Async[A] = 26 | ValueCodec.Async( 27 | encode = encode.andThen(_.asAsyncCallback), 28 | decode = decode.andThen(_.asAsyncCallback)) 29 | 30 | type ThisIsBinary = ValueCodec[A] =:= ValueCodec[BinaryData] 31 | 32 | def compress(c: Compression)(implicit ev: ThisIsBinary): ValueCodec[BinaryData] = 33 | ev(this).xmap(c.decompressOrThrow)(c.compress) 34 | } 35 | 36 | object ValueCodec { 37 | 38 | lazy val binary: ValueCodec[BinaryData] = 39 | apply( 40 | encode = b => CallbackTo.pure(b.unsafeArrayBuffer), 41 | decode = d => CallbackTo(BinaryData.unsafeFromArrayBuffer(d.asInstanceOf[ArrayBuffer])) 42 | ) 43 | 44 | lazy val boolean: ValueCodec[Boolean] = 45 | primative("Boolean") 46 | 47 | lazy val double: ValueCodec[Double] = 48 | primative("Double") 49 | 50 | lazy val int: ValueCodec[Int] = 51 | primative("Int") 52 | 53 | lazy val long: ValueCodec[Long] = 54 | string.xmap(_.toLong)(_.toString) 55 | 56 | def primative[A: ClassTag](name: String): ValueCodec[A] = 57 | apply( 58 | encode = a => CallbackTo.pure(a), 59 | decode = d => CallbackTo( 60 | (d: Any) match { 61 | case a: A => a 62 | case x => throw new js.JavaScriptException(s"Invalid IDB value found. $name expected, got: $x") 63 | } 64 | ) 65 | ) 66 | 67 | lazy val string: ValueCodec[String] = 68 | primative("String") 69 | 70 | lazy val uuid: ValueCodec[UUID] = 71 | string.xmap(UUID.fromString)(_.toString) 72 | 73 | // =================================================================================================================== 74 | 75 | final case class Async[A](encode: A => AsyncCallback[IDBValue], 76 | decode: IDBValue => AsyncCallback[A]) { 77 | 78 | def xmap[B](onDecode: A => B)(onEncode: B => A): Async[B] = 79 | // Delegating because decoding can fail and must be wrapped to be pure 80 | xmapAsync( 81 | a => AsyncCallback.delay(onDecode(a)))( 82 | b => AsyncCallback.delay(onEncode(b))) 83 | 84 | def xmapAsync[B](onDecode: A => AsyncCallback[B])(onEncode: B => AsyncCallback[A]): Async[B] = 85 | Async[B]( 86 | encode = onEncode(_).flatMap(encode), 87 | decode = decode(_).flatMap(onDecode)) 88 | 89 | type ThisIsBinary = Async[A] =:= Async[BinaryData] 90 | 91 | def xmapBinaryFormat[B](fmt: BinaryFormat[B])(implicit ev: ThisIsBinary): Async[B] = 92 | ev(this).xmapAsync(fmt.decode)(fmt.encode) 93 | } 94 | 95 | object Async { 96 | 97 | lazy val binary: ValueCodec.Async[BinaryData] = 98 | ValueCodec.binary.async 99 | 100 | def binary[A](fmt: BinaryFormat[A]): ValueCodec.Async[A] = 101 | binary.xmapBinaryFormat(fmt) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/indexeddb/package.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil 2 | 3 | package object indexeddb { 4 | 5 | type TxnDslRO = TxnDsl.RO.type 6 | type TxnDslRW = TxnDsl.RW.type 7 | 8 | @inline def TxnDslRO: TxnDslRO = TxnDsl.RO 9 | @inline def TxnDslRW: TxnDslRW = TxnDsl.RW 10 | 11 | } 12 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/locks/AbstractSharedLock.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.locks 2 | 3 | import japgolly.scalajs.react.callback.AsyncCallback 4 | import japgolly.webapputil.general.Effect 5 | import java.util.concurrent.TimeUnit 6 | import scala.concurrent.duration.FiniteDuration 7 | 8 | trait AbstractSharedLock extends GenericSharedLock.Safe.Default[AsyncCallback] { 9 | protected def unsafeRelease(): Unit 10 | /** @return await if already locked */ 11 | protected def unsafeTryAcquire(): Option[AsyncCallback[Unit]] 12 | 13 | protected final type Locked = 14 | GenericSharedLock.Safe.Locked[AsyncCallback] 15 | 16 | override final protected def F: Effect[AsyncCallback] = 17 | implicitly 18 | 19 | private val locked: Locked = 20 | GenericSharedLock.Safe.Locked(F.delay { 21 | unsafeRelease() 22 | }) 23 | 24 | private def acquire[A](onLock: AsyncCallback[A], onAwait: Option[AsyncCallback[A]]): AsyncCallback[A] = { 25 | lazy val self: AsyncCallback[A] = 26 | F.suspend { 27 | unsafeTryAcquire() match { 28 | 29 | case None => 30 | // Lock acquired 31 | onLock 32 | 33 | case Some(await) => 34 | // Mutex in use 35 | onAwait.getOrElse(F.flatMap(await)(_ => self)) 36 | } 37 | } 38 | self 39 | } 40 | 41 | override val lock: AsyncCallback[Locked] = 42 | acquire[Locked]( 43 | onLock = F.pure(locked), 44 | onAwait = None, 45 | ) 46 | 47 | override val lockInterruptibly: AsyncCallback[Locked] = 48 | lock 49 | 50 | override val tryLock: AsyncCallback[Option[Locked]] = 51 | acquire[Option[Locked]]( 52 | onLock = F.pure(Some(locked)), 53 | onAwait = Some(F.pure(None)), 54 | ) 55 | 56 | override def tryLock(time: Long, unit: TimeUnit): AsyncCallback[Option[Locked]] = 57 | F.suspend { 58 | type X = AsyncCallback[Option[Locked]] 59 | var allowRun = true 60 | val timer: X = AsyncCallback.delay {allowRun = false; None}.delay(FiniteDuration(time, unit)) 61 | val run: X = F.flatMap(lock) { l => 62 | if (allowRun) 63 | F.pure(Some(l)) 64 | else 65 | F.map(l.unlock)(_ => None) 66 | } 67 | timer.race(run).map(_.merge) 68 | } 69 | 70 | /** not re-entrant */ 71 | override def apply[A](fa: AsyncCallback[A]): AsyncCallback[A] = 72 | F.flatMap(lock)(l => fa.finallyRun(l.unlock)) 73 | } -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/locks/SharedLock.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.locks 2 | 3 | import japgolly.scalajs.react.{AsyncCallback, CallbackTo} 4 | 5 | final class SharedLock private() extends AbstractSharedLock { 6 | 7 | private var mutex: Option[AsyncCallback.Barrier] = 8 | None 9 | 10 | override protected def unsafeRelease(): Unit = 11 | for (m <- mutex) { 12 | mutex = None 13 | m.complete.runNow() 14 | } 15 | 16 | override protected def unsafeTryAcquire() = 17 | mutex match { 18 | 19 | case None => 20 | // Mutex empty 21 | val b = AsyncCallback.barrier.runNow() 22 | mutex = Some(b) 23 | None 24 | 25 | case Some(b) => 26 | // Mutex in use 27 | Some(b.await) 28 | } 29 | } 30 | 31 | // ===================================================================================================================== 32 | 33 | object SharedLock extends GenericSharedLock.Safe.ExportObjectF[AsyncCallback] { 34 | 35 | def apply(): SharedLock = 36 | new SharedLock 37 | 38 | def create: CallbackTo[SharedLock] = 39 | CallbackTo(apply()) 40 | 41 | object ReadWrite { 42 | def apply(): ReadWrite = 43 | new ReadWrite() 44 | 45 | def create: CallbackTo[ReadWrite] = 46 | CallbackTo(apply()) 47 | } 48 | 49 | // =================================================================================================================== 50 | 51 | final class ReadWrite private() extends Generic.ReadWrite[AsyncCallback, DefaultOnLock, DefaultOnTryLock] { 52 | 53 | // Whether it's a read or write mutex is determined by readers being > 0 or not 54 | private var mutex: Option[AsyncCallback.Barrier] = 55 | None 56 | 57 | private var readers = 58 | 0 59 | 60 | private def createMutex(): Unit = { 61 | assert(readers == 0) 62 | val b = AsyncCallback.barrier.runNow() 63 | mutex = Some(b) 64 | } 65 | 66 | private def releaseMutexIfNoReaders(): Unit = 67 | if (readers == 0) 68 | for (m <- mutex) { 69 | mutex = None 70 | m.complete.runNow() 71 | } 72 | 73 | override val readLock: AbstractSharedLock = 74 | new AbstractSharedLock { 75 | 76 | override protected def unsafeRelease(): Unit = { 77 | assert(readers > 0) 78 | readers -= 1 79 | releaseMutexIfNoReaders() 80 | } 81 | 82 | override protected def unsafeTryAcquire() = 83 | mutex match { 84 | 85 | case None => 86 | // Mutex empty 87 | createMutex() 88 | readers = 1 89 | None 90 | 91 | case Some(b) => 92 | if (readers > 0) { 93 | // Read-mutex in use 94 | readers += 1 95 | None 96 | 97 | } else { 98 | // Write-mutex in use 99 | Some(b.await) 100 | } 101 | } 102 | } 103 | 104 | override val writeLock: AbstractSharedLock = 105 | new AbstractSharedLock { 106 | 107 | override protected def unsafeRelease(): Unit = 108 | releaseMutexIfNoReaders() 109 | 110 | override protected def unsafeTryAcquire() = 111 | mutex match { 112 | 113 | case None => 114 | // Mutex empty 115 | createMutex() 116 | None 117 | 118 | case Some(b) => 119 | // Mutex in use 120 | Some(b.await) 121 | } 122 | } 123 | 124 | override def toString = s"SharedLock.ReadWrite($readLock, $writeLock)" 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/websocket/WebSocket.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.websocket 2 | 3 | import japgolly.microlibs.adt_macros.AdtMacros 4 | import japgolly.microlibs.utils.StaticLookupFn 5 | import japgolly.univeq.UnivEq 6 | import japgolly.webapputil.general.{Protocol, Url, VarJs} 7 | import japgolly.webapputil.websocket.WebSocketShared.CloseReason 8 | import org.scalajs.dom 9 | import org.scalajs.dom.{Blob, CloseEvent, Event, MessageEvent} 10 | import scala.scalajs.js 11 | import scala.scalajs.js.typedarray.ArrayBuffer 12 | 13 | trait WebSocket { 14 | import WebSocket._ 15 | 16 | val binaryType: VarJs[BinaryType] 17 | val onOpen : VarJs[js.Function1[Event, _]] 18 | val onMessage : VarJs[js.Function1[MessageEvent, _]] 19 | val onClose : VarJs[js.Function1[CloseEvent, _]] 20 | val onError : VarJs[js.Function1[Event, _]] 21 | 22 | def bufferedAmount(): Int 23 | def extensions(): String 24 | def readyState(): ReadyState 25 | val url: String 26 | 27 | def close(reason: CloseReason): Unit 28 | 29 | def send(data: ArrayBuffer): Unit 30 | def send(data: Blob): Unit 31 | def send(data: String): Unit 32 | } 33 | 34 | object WebSocket { 35 | 36 | def apply(underlying: dom.WebSocket): WebSocket = 37 | Real(underlying) 38 | 39 | def apply(url: Url.Absolute): WebSocket = 40 | apply(new dom.WebSocket(url.absoluteUrl)) 41 | 42 | def apply(url: Url.Absolute, protocol: String): WebSocket = 43 | apply(new dom.WebSocket(url.absoluteUrl, protocol)) 44 | 45 | def apply[Codec[_]](u: Url.Absolute.Base, p: Protocol.WebSocket.ClientReqServerPush[Codec]): WebSocket = 46 | apply(u.forWebSocket / p.url) 47 | 48 | def apply[Codec[_]](u: Url.Absolute.Base, p: Protocol.WebSocket.ClientReqServerPush[Codec], protocol: String): WebSocket = 49 | apply(u.forWebSocket / p.url, protocol) 50 | 51 | // =================================================================================================================== 52 | 53 | sealed abstract class ReadyState(final val jsValue: Int) 54 | object ReadyState { 55 | case object Connecting extends ReadyState(0) 56 | case object Open extends ReadyState(1) 57 | case object Closing extends ReadyState(2) 58 | case object Closed extends ReadyState(3) 59 | 60 | implicit def univEq: UnivEq[ReadyState] = UnivEq.derive 61 | val values = AdtMacros.adtValues[ReadyState] 62 | val byJsValue = StaticLookupFn.useArrayBy(values.whole)(_.jsValue).total 63 | } 64 | 65 | sealed abstract class BinaryType(final val jsValue: String) 66 | object BinaryType { 67 | case object Blob extends BinaryType("blob") 68 | case object ArrayBuffer extends BinaryType("arraybuffer") 69 | 70 | val values = AdtMacros.adtValues[BinaryType] 71 | val byJsValue = StaticLookupFn.useMapBy(values.whole)(_.jsValue).total 72 | } 73 | 74 | private final case class Real(underlying: dom.WebSocket) extends WebSocket { 75 | override val binaryType = VarJs.unsafeField[String](underlying, "binaryType").xmap(BinaryType.byJsValue)(_.jsValue) 76 | override val onOpen = VarJs.unsafeField[js.Function1[Event, _]] (underlying, "onopen") 77 | override val onMessage = VarJs.unsafeField[js.Function1[MessageEvent, _]](underlying, "onmessage") 78 | override val onClose = VarJs.unsafeField[js.Function1[CloseEvent, _]] (underlying, "onclose") 79 | override val onError = VarJs.unsafeField[js.Function1[Event, _]] (underlying, "onerror") 80 | 81 | override def bufferedAmount() = underlying.bufferedAmount 82 | override def extensions() = underlying.extensions 83 | override def readyState() = ReadyState.byJsValue(underlying.readyState) 84 | override val url = underlying.url 85 | 86 | override def close(reason: CloseReason) = underlying.close(reason.code.value, reason.phrase.value) 87 | 88 | override def send(data: ArrayBuffer) = underlying.send(data) 89 | override def send(data: Blob) = underlying.send(data) 90 | override def send(data: String) = underlying.send(data) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/webstorage/KeyCodec.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.webstorage 2 | 3 | import japgolly.scalajs.react.CallbackTo 4 | import japgolly.webapputil.webstorage.AbstractWebStorage.Key 5 | 6 | final case class KeyCodec[A](encode: A => Key, 7 | decode: Key => CallbackTo[A]) { 8 | 9 | def xmap[B](onDecode: A => B)(onEncode: B => A): KeyCodec[B] = 10 | // Delegating because decoding can fail and must be wrapped to be pure 11 | xmapSync( 12 | a => CallbackTo(onDecode(a)))( 13 | onEncode) 14 | 15 | def xmapSync[B](onDecode: A => CallbackTo[B])(onEncode: B => A): KeyCodec[B] = 16 | KeyCodec[B]( 17 | encode = encode compose onEncode, 18 | decode = decode(_).flatMap(onDecode)) 19 | } 20 | 21 | object KeyCodec { 22 | 23 | val string: KeyCodec[String] = 24 | apply(Key.apply, k => CallbackTo.pure(k.value)) 25 | 26 | lazy val int: KeyCodec[Int] = 27 | string.xmap(_.toInt)(_.toString) 28 | } 29 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/webstorage/ValueCodec.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.webstorage 2 | 3 | import japgolly.scalajs.react.{AsyncCallback, CallbackTo} 4 | import japgolly.webapputil.binary._ 5 | import japgolly.webapputil.webstorage.AbstractWebStorage.{Key, Value} 6 | 7 | final case class ValueCodec[A](encode: A => CallbackTo[Value], 8 | decode: Value => CallbackTo[A]) { 9 | 10 | def xmap[B](onDecode: A => B)(onEncode: B => A): ValueCodec[B] = 11 | // Delegating because decoding can fail and must be wrapped to be pure 12 | xmapSync( 13 | a => CallbackTo(onDecode(a)))( 14 | b => CallbackTo(onEncode(b))) 15 | 16 | def xmapSync[B](onDecode: A => CallbackTo[B])(onEncode: B => CallbackTo[A]): ValueCodec[B] = 17 | ValueCodec[B]( 18 | encode = onEncode(_).flatMap(encode), 19 | decode = decode(_).flatMap(onDecode)) 20 | 21 | def async: ValueCodec.Async[A] = 22 | ValueCodec.Async( 23 | encode = encode.andThen(_.asAsyncCallback), 24 | decode = decode.andThen(_.asAsyncCallback)) 25 | } 26 | 27 | object ValueCodec { 28 | 29 | final case class Async[A](encode: A => AsyncCallback[Value], 30 | decode: Value => AsyncCallback[A]) { 31 | 32 | def xmap[B](onDecode: A => B)(onEncode: B => A): Async[B] = 33 | // Delegating because decoding can fail and must be wrapped to be pure 34 | xmapAsync( 35 | a => AsyncCallback.delay(onDecode(a)))( 36 | b => AsyncCallback.delay(onEncode(b))) 37 | 38 | def xmapAsync[B](onDecode: A => AsyncCallback[B])(onEncode: B => AsyncCallback[A]): Async[B] = 39 | Async[B]( 40 | encode = onEncode(_).flatMap(encode), 41 | decode = decode(_).flatMap(onDecode)) 42 | 43 | def xmapRaw(afterEncode : (A, Value) => Value, 44 | beforeDecode: Value => Value): Async[A] = 45 | Async[A]( 46 | encode = a => encode(a).map(afterEncode(a, _)), 47 | decode = v => decode(beforeDecode(v))) 48 | 49 | def webStorageKey(key: Key): WebStorageKey.Async[A] = 50 | WebStorageKey.Async(key, this) 51 | } 52 | 53 | // =================================================================================================================== 54 | // Instances 55 | 56 | val string: ValueCodec[String] = 57 | apply( 58 | encode = s => CallbackTo.pure(Value(s)), 59 | decode = r => CallbackTo.pure(r.value)) 60 | 61 | lazy val boolean: ValueCodec[Boolean] = 62 | string.xmap({ 63 | case "1" => true 64 | case "0" => false 65 | })({ 66 | case true => "1" 67 | case false => "0" 68 | }) 69 | 70 | def binaryString(implicit enc: BinaryString.Encoder): ValueCodec[BinaryString] = 71 | string.xmap(BinaryString.encoded)(_.encoded) 72 | 73 | def binary(implicit enc: BinaryString.Encoder): ValueCodec[BinaryData] = 74 | binaryString.xmap(_.binaryValue)(BinaryString.apply) 75 | 76 | object Async { 77 | 78 | def binary(implicit enc: BinaryString.Encoder): ValueCodec.Async[BinaryData] = 79 | ValueCodec.binary.async 80 | 81 | def binaryFormat[A](fmt: BinaryFormat[A])(implicit enc: BinaryString.Encoder): ValueCodec.Async[A] = 82 | binary.xmapAsync(fmt.decode)(fmt.encode) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/webstorage/WebStorageKey.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.webstorage 2 | 3 | import japgolly.scalajs.react.{AsyncCallback, Callback, CallbackTo} 4 | import japgolly.webapputil.binary._ 5 | import japgolly.webapputil.webstorage.AbstractWebStorage.Key 6 | 7 | final case class WebStorageKey[V](key: Key, valueCodec: ValueCodec[V]) { 8 | 9 | def get(implicit s: AbstractWebStorage): CallbackTo[Option[V]] = 10 | s.getItem(key).flatMap(CallbackTo.traverseOption(_)(valueCodec.decode)) 11 | 12 | def set(value: V)(implicit s: AbstractWebStorage): Callback = 13 | valueCodec.encode(value).flatMap(s.setItem(key, _)) 14 | 15 | def remove(implicit s: AbstractWebStorage): Callback = 16 | s.removeItem(key) 17 | 18 | def setOrRemove(value: Option[V])(implicit s: AbstractWebStorage): Callback = 19 | value.fold(remove)(set(_)) 20 | } 21 | 22 | object WebStorageKey { 23 | 24 | final case class Async[V](key: Key, valueCodec: ValueCodec.Async[V]) { 25 | 26 | def get(implicit s: AbstractWebStorage): AsyncCallback[Option[V]] = 27 | s.getItem(key).asAsyncCallback.flatMap(AsyncCallback.traverseOption(_)(valueCodec.decode)) 28 | 29 | def set(value: V)(implicit s: AbstractWebStorage): AsyncCallback[Unit] = 30 | valueCodec.encode(value).flatMap(s.setItem(key, _).asAsyncCallback) 31 | 32 | def remove(implicit s: AbstractWebStorage): Callback = 33 | s.removeItem(key) 34 | 35 | def setOrRemove(value: Option[V])(implicit s: AbstractWebStorage): AsyncCallback[Unit] = 36 | value.fold(remove.asAsyncCallback)(set(_)) 37 | } 38 | 39 | // =================================================================================================================== 40 | // Convenience methods 41 | 42 | def string(key: String): WebStorageKey[String] = 43 | new WebStorageKey(Key(key), ValueCodec.string) 44 | 45 | def boolean(key: String): WebStorageKey[Boolean] = 46 | new WebStorageKey(Key(key), ValueCodec.boolean) 47 | 48 | def binaryString(key: String)(implicit enc: BinaryString.Encoder): WebStorageKey[BinaryString] = 49 | new WebStorageKey(Key(key), ValueCodec.binaryString) 50 | 51 | def binary(key: String)(implicit enc: BinaryString.Encoder): WebStorageKey[BinaryData] = 52 | new WebStorageKey(Key(key), ValueCodec.binary) 53 | 54 | object Async { 55 | 56 | def binary(key: String)(implicit enc: BinaryString.Encoder): Async[BinaryData] = 57 | Async(Key(key), ValueCodec.Async.binary) 58 | 59 | def binaryFormat[A](key: String, fmt: BinaryFormat[A])(implicit enc: BinaryString.Encoder): Async[A] = 60 | Async(Key(key), ValueCodec.Async.binaryFormat(fmt)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/webworker/OnError.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.webworker 2 | 3 | import japgolly.scalajs.react.Callback 4 | import japgolly.webapputil.general.ErrorMsg 5 | import org.scalajs.dom.console 6 | 7 | final case class OnError(handle: ErrorMsg => Callback) extends AnyVal { 8 | @inline def apply(e: ErrorMsg) = handle(e) 9 | } 10 | 11 | object OnError { 12 | def logToConsole: OnError = 13 | OnError(err => Callback(console.error(err))) 14 | } 15 | -------------------------------------------------------------------------------- /core/js/src/main/scala/japgolly/webapputil/webworker/WebWorkerProtocol.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.webworker 2 | 3 | import org.scalajs.dom.Transferable 4 | import scala.scalajs.js 5 | 6 | trait WebWorkerProtocol { 7 | 8 | /** Type of an encoded message */ 9 | type Encoded 10 | 11 | /** Type-class for serialising messages */ 12 | type Encoder[A] 13 | 14 | /** Type-class for deserialising messages */ 15 | type Decoder[A] 16 | 17 | def encode[A: Encoder](input: A): Encoded 18 | 19 | def decode[A: Decoder](encoded: Encoded): A 20 | 21 | def transferables(e: Encoded): js.UndefOr[js.Array[Transferable]] 22 | } 23 | 24 | object WebWorkerProtocol { 25 | 26 | type WithEncoded[E] = WebWorkerProtocol { type Encoded = E } 27 | 28 | type WithEncoder[F[_]] = WebWorkerProtocol { type Encoder[A] = F[A] } 29 | 30 | type WithDecoder[F[_]] = WebWorkerProtocol { type Decoder[A] = F[A] } 31 | 32 | type WithCodecs[Enc[_], Dec[_]] = WebWorkerProtocol { 33 | type Encoder[A] = Enc[A] 34 | type Decoder[A] = Dec[A] 35 | } 36 | 37 | type Full[E, Enc[_], Dec[_]] = WebWorkerProtocol { 38 | type Encoded = E 39 | type Encoder[A] = Enc[A] 40 | type Decoder[A] = Dec[A] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/js/src/test/scala/japgolly/webapputil/binary/BinaryDataJsTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.binary 2 | 3 | import japgolly.microlibs.testutil.TestUtil._ 4 | import nyaya.gen.Gen 5 | import scala.scalajs.js 6 | import scala.scalajs.js.typedarray.{Int8Array, TypedArrayBuffer} 7 | import utest._ 8 | 9 | object BinaryDataJsTest extends TestSuite { 10 | 11 | private val bytes = 12 | Gen.shuffle(Byte.MinValue.to(Byte.MaxValue).map(_.toByte).toList).sample() 13 | 14 | private def bd = 15 | BinaryData.fromArray(bytes.toArray) 16 | 17 | override def tests = Tests { 18 | 19 | "noOffset" - { 20 | def expected = bd 21 | 22 | "fromArrayBuffer" - assertEq( 23 | BinaryData.fromArrayBuffer(bd.toArrayBuffer), 24 | expected) 25 | 26 | "unsafeFromArrayBuffer" - assertEq( 27 | BinaryData.unsafeFromArrayBuffer(bd.toArrayBuffer), 28 | expected) 29 | 30 | "typedArray" - { 31 | val buffer = Int8Array.from(js.Array(3, 4, 5)).buffer 32 | val view = new Int8Array(buffer) 33 | val bb = TypedArrayBuffer.wrap(view) 34 | val ia = BinaryJs.byteBufferToInt8Array(bb) 35 | val ab = BinaryJs.int8ArrayToArrayBuffer(ia) 36 | assertEq( 37 | BinaryData.unsafeFromArrayBuffer(ab), 38 | BinaryData.unsafeFromArray(Array(3, 4, 5))) 39 | } 40 | } 41 | 42 | // ----------------------------------------------------------------------------------------------------------------- 43 | "offset" - { 44 | def bd1 = bd.drop(1) 45 | def expected = BinaryData.unsafeFromArray(bd.unsafeArray.drop(1)) 46 | 47 | "fromArrayBuffer" - assertEq( 48 | BinaryData.fromArrayBuffer(bd1.toArrayBuffer), 49 | expected) 50 | 51 | "unsafeFromArrayBuffer" - assertEq( 52 | BinaryData.unsafeFromArrayBuffer(bd1.toArrayBuffer), 53 | expected) 54 | 55 | "typedArray" - { 56 | val buffer = Int8Array.from(js.Array(1, 2, 3, 4, 5)).buffer 57 | val view = new Int8Array(buffer, 2) 58 | val bb = TypedArrayBuffer.wrap(view) 59 | val ia = BinaryJs.byteBufferToInt8Array(bb) 60 | val ab = BinaryJs.int8ArrayToArrayBuffer(ia) 61 | assertEq( 62 | BinaryData.unsafeFromArrayBuffer(ab), 63 | BinaryData.unsafeFromArray(Array(3, 4, 5))) 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /core/js/src/test/scala/japgolly/webapputil/general/AsyncFunctionTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.microlibs.testutil.TypeTestingUtil._ 4 | 5 | sealed trait AsyncFunctionTest { 6 | type I 7 | type E 8 | type A 9 | def e: E 10 | 11 | assertType[AsyncFunction[I, E, A]] 12 | .map(_.attempt) 13 | .is[AsyncFunction[I, Either[Throwable, E], A]] 14 | 15 | assertType[AsyncFunction[I, Nothing, A]] 16 | .map(_.attempt) 17 | .is[AsyncFunction[I, Throwable, A]] 18 | 19 | assertType[AsyncFunction[I, E, A]] 20 | .map(_.attemptInto(_ => e)) 21 | .is[AsyncFunction[I, E, A]] 22 | 23 | assertType[AsyncFunction[I, Either[E, E], A]] 24 | .map(_.mergeErrors) 25 | .is[AsyncFunction[I, E, A]] 26 | 27 | } 28 | -------------------------------------------------------------------------------- /core/js/src/test/scala/japgolly/webapputil/general/VarJsTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.microlibs.testutil.TestUtil._ 4 | import japgolly.webapputil.webstorage.AbstractWebStorage 5 | import utest._ 6 | 7 | object VarJsTest extends TestSuite { 8 | 9 | override def tests = Tests { 10 | 11 | "var" - { 12 | val v = VarJs(1) 13 | assertEq(v.unsafeGet(), 1) 14 | v.unsafeSet(3) 15 | assertEq(v.unsafeGet(), 3) 16 | } 17 | 18 | "webStorage" - { 19 | "boolean" - { 20 | val s = AbstractWebStorage.inMemory() 21 | val k = AbstractWebStorage.Key("blah") 22 | val v1 = VarJs.webStorage.boolean(s, k) 23 | val v2 = VarJs.webStorage.boolean(s, k) 24 | def get() = (v1.unsafeGet(), v2.unsafeGet()) 25 | assertEq(get(), (false, false)) 26 | v1.unsafeSet(true) 27 | assertEq(get(), (true, true)) 28 | v2.unsafeSet(false) 29 | assertEq(get(), (false, false)) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/js/src/test/scala/japgolly/webapputil/webstorage/WebStorageTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.webstorage 2 | 3 | import japgolly.microlibs.testutil.TestUtil._ 4 | import japgolly.webapputil.binary._ 5 | import japgolly.webapputil.webstorage.AbstractWebStorage.Key 6 | import utest._ 7 | 8 | object WebStorageTest extends TestSuite { 9 | 10 | private implicit val encoder: BinaryString.Encoder = 11 | BinaryString.Base32768.global 12 | 13 | override def tests = Tests { 14 | 15 | "binary" - { 16 | val key = WebStorageKey(Key("omg"), ValueCodec.binary) 17 | 18 | implicit val ws = AbstractWebStorage.inMemory() 19 | 20 | assertEq(key.get.runNow(), None) 21 | 22 | val bin1 = BinaryData.fromHex("9876543210abcdef") 23 | key.set(bin1).runNow() 24 | assertEq(key.get.runNow(), Some(bin1)) 25 | 26 | val bin2 = BinaryData.fromHex("7418529630") 27 | key.set(bin2).runNow() 28 | assertEq(key.get.runNow(), Some(bin2)) 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/japgolly/webapputil/binary/BinaryData_PlatformSpecific.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.binary 2 | 3 | // *********** 4 | // * * 5 | // * JVM * 6 | // * * 7 | // *********** 8 | 9 | trait BinaryData_PlatformSpecific_Object { self: BinaryData.type => 10 | } 11 | 12 | trait BinaryData_PlatformSpecific_Instance { self: BinaryData => 13 | } 14 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/japgolly/webapputil/entrypoint/EntrypointInvoker.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.entrypoint 2 | 3 | import japgolly.microlibs.stdlib_ext.EscapeUtils 4 | import java.lang.{StringBuilder => JStringBuilder} 5 | 6 | object EntrypointInvoker { 7 | 8 | def apply[I](defn: EntrypointDef[I]): EntrypointInvoker[I] = 9 | new EntrypointInvoker(defn) 10 | 11 | // TODO Potential optimisation: have this estimate a good initial SB size for itself by observing past results 12 | private[EntrypointInvoker] final val ExpectedJsLength = 256 13 | } 14 | 15 | final class EntrypointInvoker[Input](defn: EntrypointDef[Input]) { 16 | import EntrypointInvoker.ExpectedJsLength 17 | 18 | private val runCmdHead = 19 | defn.objectAndMethod + "(\"" 20 | 21 | private val appendEncoded: (JStringBuilder, String) => Unit = 22 | if (defn.codec.escapeEncodedString) 23 | EscapeUtils.appendEscaped 24 | else 25 | _.append(_) 26 | 27 | private def call(sb: JStringBuilder, i: Input): Unit = { 28 | sb.append(runCmdHead) 29 | appendEncoded(sb, defn.codec.encode(i)) 30 | sb.append("\")") 31 | } 32 | 33 | def apply(i: Input): Js = { 34 | val sb = new JStringBuilder(ExpectedJsLength) 35 | call(sb, i) 36 | Js(sb.toString) 37 | } 38 | 39 | def apply(w: Js.Wrapper, i: Input): Js = { 40 | val sb = new JStringBuilder(ExpectedJsLength + w.totalLength) 41 | sb.append(w.before) 42 | call(sb, i) 43 | sb.append(w.after) 44 | Js(sb.toString) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/japgolly/webapputil/entrypoint/Html.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.entrypoint 2 | 3 | import japgolly.univeq.UnivEq 4 | 5 | final case class Html(val asString: String) extends AnyVal 6 | 7 | object Html { 8 | implicit def univEq: UnivEq[Html] = 9 | UnivEq.derive 10 | } 11 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/japgolly/webapputil/entrypoint/LoadJs.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.entrypoint 2 | 3 | import java.lang.{StringBuilder => JStringBuilder} 4 | 5 | /** Allows for loading resources after the page has been rendered, uses loadjs (https://github.com/muicss/loadjs). 6 | * 7 | * This allows resource-heavy pages to load quickly and render "Loading" to the user, 8 | * before loading and parsing all the resources and initialising the SPA. 9 | * 10 | * Usage: Create a [[LoadJs.Bundle]] and pass it to [[EntrypointInvoker]]. 11 | */ 12 | object LoadJs { 13 | 14 | final class Resource(val href: String, val integrity: Option[String]) { 15 | def absoluteUrl: Boolean = 16 | href contains "://" 17 | 18 | val crossOrigin: Option[String] = 19 | Option.when(absoluteUrl)("anonymous") 20 | } 21 | 22 | object Resource { 23 | def apply(href: String): Resource = 24 | new Resource(href, None) 25 | 26 | def apply(href: String, integrity: Option[String]): Resource = 27 | new Resource(href, integrity) 28 | } 29 | 30 | // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 31 | 32 | object Bundle { 33 | def apply(rs: Resource*): Bundle = 34 | new Bundle(rs) 35 | } 36 | 37 | final class Bundle(resources: Iterable[Resource]) { 38 | 39 | assert(resources.nonEmpty, "Empty bundles aren't yet handled.") 40 | 41 | assert( 42 | resources.size == resources.toList.map(_.href).toSet.size, 43 | s"Duplicate hrefs detected in: ${resources.toList.map(_.href).sorted}") 44 | 45 | val jsWrapper: Js.Wrapper = { 46 | val head: String = { 47 | // Because this is stored as val and Resources bundles are static, efficiency here doesn't matter 48 | val sb = new JStringBuilder 49 | val rs = resources.toArray 50 | 51 | assert(rs.length <= 22) // because terms are a,b,c,… and xyz are reserved 52 | def term(idx: Int) = ('a' + idx).toChar 53 | 54 | // Create variables for each URL - reduces JS size cos they're referenced more than once 55 | sb append "var " 56 | for (i <- rs.indices) { 57 | if (i != 0) sb append ',' 58 | sb append s"${term(i)}='${rs(i).href}'" // should really escape href 59 | } 60 | sb append ';' 61 | 62 | // Determine additional tag attributes per resource 63 | val extraCfg: Map[Char, List[String]] = 64 | rs.indices.map { i => 65 | var cfg = List.empty[String] 66 | val r = rs(i) 67 | r.integrity.foreach(a => cfg ::= s"x.integrity='$a'") 68 | r.crossOrigin.foreach(a => cfg ::= s"x.crossOrigin='$a'") 69 | term(i) -> cfg 70 | } 71 | .filter(_._2.nonEmpty) 72 | .toMap 73 | 74 | sb append s"loadjs([${rs.indices.map(term).mkString(",")}],{" 75 | if (extraCfg.nonEmpty) { 76 | sb append "before:function(z,x){switch(z){" 77 | for ((term, cfg) <- extraCfg) { 78 | sb append cfg.mkString(s"case $term:", ";", ";break;") 79 | } 80 | sb append "}}," 81 | } 82 | sb append "async:!1," // Fetch files in parallel and load them in series 83 | sb append "success:function(){" 84 | sb.toString 85 | } 86 | 87 | Js.Wrapper(head, "}})") 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/japgolly/webapputil/general/Effect_PlatformSpecific.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | trait Effect_PlatformSpecific 4 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/japgolly/webapputil/http/UrlEncoder.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.http 2 | 3 | import java.net.{URLDecoder, URLEncoder} 4 | import java.nio.charset.StandardCharsets 5 | 6 | object UrlEncoder extends UrlEncoderApi { 7 | 8 | private[this] val utf8 = StandardCharsets.UTF_8 9 | 10 | override def encode(s: String): String = 11 | URLEncoder.encode(s, utf8) 12 | 13 | override def decode(s: String): String = 14 | URLDecoder.decode(s, utf8) 15 | } 16 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/japgolly/webapputil/locks/LockMechanism.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.locks 2 | 3 | import java.util.concurrent.locks.Lock 4 | import java.util.concurrent.{TimeUnit, TimeoutException} 5 | 6 | sealed trait LockMechanism { 7 | def lock(l: Lock): Unit 8 | } 9 | 10 | object LockMechanism { 11 | 12 | implicit val default: LockMechanism = 13 | Interruptibly 14 | 15 | case object Interruptibly extends LockMechanism { 16 | override def lock(l: Lock): Unit = 17 | l.lockInterruptibly() 18 | } 19 | 20 | final case class LimitWaitTime(time: Long, unit: TimeUnit, lockName: String = null) extends LockMechanism { 21 | override def lock(l: Lock): Unit = 22 | if (!l.tryLock(time, unit)) { 23 | val name = Option(lockName).fold("lock")(_ + " lock") 24 | throw new TimeoutException(s"Failed to aquire $name in $time $unit") 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/japgolly/webapputil/locks/LockUtils.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.locks 2 | 3 | import japgolly.webapputil.general.Effect 4 | import java.util.concurrent.locks.Lock 5 | 6 | object LockUtils { 7 | 8 | def inMutex[A](lock: Lock)(a: => A)(implicit m: LockMechanism): A = { 9 | m.lock(lock) 10 | try a finally lock.unlock() 11 | } 12 | 13 | def maybeInMutex[A](mutex: Option[Lock])(a: => A)(implicit m: LockMechanism): A = 14 | mutex.fold(a)(inMutex(_)(a)) 15 | 16 | def inMutexF[F[_], A](lock: Lock)(fa: F[A])(implicit F: Effect[F], m: LockMechanism): F[A] = { 17 | val start = F.delay(m.lock(lock)) 18 | val stop = F.delay(lock.unlock()) 19 | F.bracket(start)(use = _ => fa)(release = _ => stop) 20 | } 21 | 22 | def maybeInMutexF[F[_]: Effect, A](mutex: Option[Lock])(io: F[A])(implicit m: LockMechanism): F[A] = 23 | mutex.fold(io)(inMutexF(_)(io)) 24 | } 25 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/japgolly/webapputil/locks/SharedLock.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.locks 2 | 3 | import java.util.concurrent.TimeUnit 4 | import java.util.concurrent.locks.StampedLock 5 | 6 | trait SharedLock extends GenericSharedLock.Unsafe.Default 7 | 8 | object SharedLock extends GenericSharedLock.Unsafe.ExportObject { 9 | 10 | def apply(): SharedLock = 11 | Stamped.write() 12 | 13 | object ReadWrite { 14 | def apply(): ReadWrite = 15 | Stamped.readWrite() 16 | } 17 | 18 | // =================================================================================================================== 19 | object Stamped { 20 | def read(s: StampedLock = new StampedLock()): SharedLock = 21 | this(s, Access.Read) 22 | 23 | def write(s: StampedLock = new StampedLock()): SharedLock = 24 | this(s, Access.Write) 25 | 26 | def readWrite(r: StampedLock = new StampedLock(), w: StampedLock = new StampedLock()): ReadWrite = 27 | ReadWrite(readLock = read(r), writeLock = write(w)) 28 | 29 | private def apply(s: StampedLock, a: Access): SharedLock = 30 | new Lock(a(s)) 31 | 32 | private trait StampedApi extends Generic[Long, Long] { 33 | def show(): String 34 | def unlock(stamp: Long): Unit 35 | } 36 | 37 | private final class Lock(s: StampedApi) extends SharedLock { 38 | override def toString = s.show() 39 | 40 | private def wrapLock(stamp: Long): Locked = 41 | Locked(() => s.unlock(stamp)) 42 | 43 | private def wrapTry(stamp: Long): Option[Locked] = 44 | Option.unless(stamp == 0L)(wrapLock(stamp)) 45 | 46 | override def lock() = wrapLock(s.lock()) 47 | override def lockInterruptibly() = wrapLock(s.lockInterruptibly()) 48 | override def tryLock() = wrapTry(s.tryLock()) 49 | override def tryLock(t: Long, u: TimeUnit) = wrapTry(s.tryLock(t, u)) 50 | } 51 | 52 | private sealed trait Access { 53 | def apply(s: StampedLock): StampedApi 54 | } 55 | 56 | private object Access { 57 | 58 | case object Read extends Access { 59 | override def apply(s: StampedLock): StampedApi = new StampedApi { 60 | override def lock() = s.readLock() 61 | override def lockInterruptibly() = s.readLockInterruptibly() 62 | override def tryLock() = s.tryReadLock() 63 | override def tryLock(t: Long, u: TimeUnit) = s.tryReadLock(t, u) 64 | override def unlock(i: Long) = s.unlockRead(i) 65 | override def show() = s"SharedLock.read($s)" 66 | } 67 | } 68 | 69 | case object Write extends Access { 70 | override def apply(s: StampedLock): StampedApi = new StampedApi { 71 | override def lock() = s.writeLock() 72 | override def lockInterruptibly() = s.writeLockInterruptibly() 73 | override def tryLock() = s.tryWriteLock() 74 | override def tryLock(t: Long, u: TimeUnit) = s.tryWriteLock(t, u) 75 | override def unlock(i: Long) = s.unlockWrite(i) 76 | override def show() = s"SharedLock.write($s)" 77 | } 78 | } 79 | } 80 | } 81 | // =================================================================================================================== 82 | 83 | case class ReadWrite(override val readLock : SharedLock, 84 | override val writeLock: SharedLock) 85 | extends Generic.ReadWrite[DefaultOnLock, DefaultOnTryLock] { 86 | override def toString = s"SharedLock.ReadWrite($readLock, $writeLock)" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/japgolly/webapputil/websocket/WebSocketServerUtil.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.websocket 2 | 3 | import japgolly.webapputil.http.Cookie 4 | import javax.websocket.server._ 5 | import scala.jdk.CollectionConverters._ 6 | 7 | object WebSocketServerUtil { 8 | 9 | def cookieLookupFnOverHandshakeRequest(req: HandshakeRequest): Cookie.LookupFn = 10 | req.getHeaders.get("cookie").asScala.headOption match { 11 | case Some(cookieStr) => Cookie.LookupFn.overHeader(cookieStr) 12 | case None => _ => None 13 | } 14 | 15 | // Doesn't work -- https://github.com/eclipse/jetty.project/issues/3575 16 | // def pathParam(req: HandshakeRequest, name: String): String = 17 | // req.getParameterMap.get(name).asScala.headOption.getOrElse { 18 | // throw new IllegalStateException(s"WebSocket PathParam '$name' not found. uri=${req.getRequestURI}") 19 | // } 20 | 21 | object CloseReasons { 22 | import WebSocketShared._ 23 | 24 | val errorParsingMessage = CloseReason(CloseCode.protocolError, CloseReasonPhrase("Error parsing message")) 25 | val errorParsingSubscriptionData = CloseReason(CloseCode.unexpectedCondition, CloseReasonPhrase("Error parsing subscription data")) 26 | val errorSendingResponse = CloseReason(CloseCode.respondException, CloseReasonPhrase("Error sending response")) 27 | val invalidRequest = CloseReason(CloseCode.cannotAccept, CloseReasonPhrase("Invalid request")) 28 | val runtimeExceptionOccurred = CloseReason(CloseCode.unhandledException, CloseReasonPhrase("Runtime exception occurred")) 29 | val serverOutOfDate = CloseReason(CloseCode.serviceRestart, CloseReasonPhrase("Server is out-of-date")) 30 | val unauthorised = CloseReason(CloseCode.unauthorised, CloseReasonPhrase("Unauthorised")) 31 | } 32 | 33 | class CustomCloseCode(code: Int) extends javax.websocket.CloseReason.CloseCode { 34 | override final def getCode = code 35 | } 36 | 37 | object CustomCloseCode { 38 | def apply(code: Int): CustomCloseCode = 39 | new CustomCloseCode(code) 40 | } 41 | 42 | // =================================================================================================================== 43 | 44 | /** The return type of [[javax.websocket.EndpointConfig#getUserProperties()]] */ 45 | type UserProps = java.util.Map[String, AnyRef] 46 | 47 | final case class UserPropsLens[A](get: UserProps => A, set: (UserProps, A) => Unit) 48 | 49 | object UserPropsLens { 50 | 51 | def atKey[A <: AnyRef](key: String): UserPropsLens[A] = 52 | new UserPropsLens[A]( 53 | _.get(key).asInstanceOf[A], 54 | _.put(key, _)) 55 | 56 | def atKey[A <: AnyRef](key: String, default: => A): UserPropsLens[A] = 57 | new UserPropsLens[A]( 58 | p => {val v = p.get(key); if (v eq null) default else v.asInstanceOf[A]}, 59 | _.put(key, _)) 60 | } 61 | 62 | // =================================================================================================================== 63 | 64 | object Implicits { 65 | implicit def closeReasonToJavax(r: WebSocketShared.CloseReason): javax.websocket.CloseReason = 66 | new javax.websocket.CloseReason(CustomCloseCode(r.code.value), r.phrase.value) 67 | } 68 | } -------------------------------------------------------------------------------- /core/jvm/src/test/scala/japgolly/webapputil/entrypoint/EntrypointInvokerTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.entrypoint 2 | 3 | import japgolly.microlibs.testutil.TestUtil._ 4 | import japgolly.webapputil.binary.BinaryData 5 | import utest._ 6 | 7 | object EntrypointInvokerTest extends TestSuite { 8 | import EntrypointDef.Codec.ClearText._ 9 | 10 | private val script = """script type="text/javascript"""" 11 | private val d = EntrypointDef[String]("XX") 12 | 13 | override def tests = Tests { 14 | 15 | "scriptInline" - { 16 | val js = EntrypointInvoker(d)("console.log('')") 17 | 18 | "base64" - { 19 | val b64 = "WFgubSgiY29uc29sZS5sb2coJzwvc2NyaXB0PicpIik=" 20 | 21 | assertEq( 22 | js.scriptInlineBase64.asString, 23 | s"""""") 24 | 25 | assertEq( 26 | BinaryData.fromBase64OrThrow(b64).toStringAsUtf8, 27 | """XX.m("console.log('')")""") 28 | } 29 | 30 | "escaped" - assertEq( 31 | js.scriptInlineEscaped.asString, 32 | s"""<$script src="data:application/javascript,XX.m("console.log('</script>')")">""") 33 | } 34 | 35 | "scriptOnLoad" - { 36 | val js = EntrypointInvoker(d)("hello") 37 | val onload = """onload="XX.m("hello")"""" 38 | 39 | "basic" - assertEq( 40 | js.scriptOnLoad("//blah.js").asString, 41 | s"""<$script src="//blah.js" $onload>""") 42 | 43 | "async" - assertEq( 44 | js.scriptOnLoad("//blah.js", async = true).asString, 45 | s"""<$script async="async" src="//blah.js" $onload>""") 46 | 47 | "defer" - assertEq( 48 | js.scriptOnLoad("//blah.js", defer = true).asString, 49 | s"""<$script defer="defer" src="//blah.js" $onload>""") 50 | 51 | "integrity" - assertEq( 52 | js.scriptOnLoad("//blah.js", integrity = "123").asString, 53 | s"""<$script integrity="123" src="//blah.js" $onload>""") 54 | 55 | "crossorigin" - assertEq( 56 | js.scriptOnLoad("//blah.js", crossorigin = "abc").asString, 57 | s"""<$script crossorigin="abc" src="//blah.js" $onload>""") 58 | } 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/ajax/AjaxProtocol.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.ajax 2 | 3 | import japgolly.webapputil.general.{Protocol, Url} 4 | 5 | object AjaxProtocol { 6 | 7 | /** Simple because the response protocol is static and independent of request data. 8 | * This is opposed to dependently-typed protocols where the response protocol depends on the request value. 9 | * 10 | * Note: the `F[_]` type parameter is the message encoder (eg. `JsonCodec`, `boopickle.Pickler`) 11 | */ 12 | final case class Simple[F[_], _Req, _Res](url: Url.Relative, 13 | req: Protocol.Of[F, _Req], 14 | res: Protocol.Of[F, _Res]) extends AjaxProtocol[F] { 15 | type Req = _Req 16 | type Res = _Res 17 | override val protocol: Protocol.RequestResponse.Simple[F, Req, Res] = Protocol.RequestResponse.simple(res) 18 | override val requestProtocol = req 19 | override def responseProtocol(req: Req) = res 20 | } 21 | } 22 | 23 | /** Note: the `F[_]` type parameter is the message encoder (eg. `JsonCodec`, `boopickle.Pickler`) 24 | */ 25 | trait AjaxProtocol[F[_]] { 26 | val url : Url.Relative 27 | val protocol : Protocol.RequestResponse[F] 28 | val requestProtocol: Protocol.Of[F, protocol.PreparedRequestType] 29 | 30 | def responseProtocol(req: protocol.PreparedRequestType): Protocol.Of[F, protocol.ResponseType] 31 | 32 | final type ServerSideFn [G[_] ] = protocol.PreparedRequestType => G[ protocol.ResponseType] 33 | final type ServerSideFnI [G[_], I ] = (I, protocol.PreparedRequestType) => G[ protocol.ResponseType] 34 | final type ServerSideFnO [G[_], O] = protocol.PreparedRequestType => G[(protocol.ResponseType, O)] 35 | final type ServerSideFnIO[G[_], I, O] = (I, protocol.PreparedRequestType) => G[(protocol.ResponseType, O)] 36 | } 37 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/binary/CodecEngine.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.binary 2 | 3 | /** Capability to encode and decode binary data given a codec typeclass `F[_]` */ 4 | trait CodecEngine[F[_], +E] { self => 5 | 6 | def encode[A](a: A)(implicit codec: F[A]): BinaryData 7 | def decode[A](b: BinaryData)(implicit codec: F[A]): Either[E, A] 8 | 9 | def mapError[X](f: E => X): CodecEngine[F, X] = 10 | new CodecEngine[F, X] { 11 | 12 | override def encode[A](a: A)(implicit codec: F[A]): BinaryData = 13 | self.encode(a) 14 | 15 | override def decode[A](b: BinaryData)(implicit codec: F[A]): Either[X, A] = 16 | self.decode[A](b).left.map(f) 17 | } 18 | } 19 | 20 | object CodecEngine 21 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/general/AbstractMultiStringMap.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.univeq.UnivEq 4 | import scala.jdk.CollectionConverters._ 5 | 6 | /** Equivalent to a `Map[String, List[String]]`. 7 | * 8 | * This is abstract for easy newtype creation. 9 | */ 10 | abstract class AbstractMultiStringMap[Self](final val asVector: Vector[(String, String)], final val isNormalised: Boolean) { self: Self => 11 | 12 | final type Self2 = Self with AbstractMultiStringMap[Self] 13 | 14 | protected def create(asVector: Vector[(String, String)], isNormalised: Boolean = false): Self2 15 | 16 | protected def displayName: String = 17 | getClass.getSimpleName 18 | 19 | final def isEmpty = asVector.isEmpty 20 | final def nonEmpty = asVector.nonEmpty 21 | 22 | def get(key: String): Vector[String] = 23 | asVector.iterator.filter(_._1 == key).map(_._2).toVector 24 | 25 | def add(key: String, value: String): Self = 26 | create(asVector :+ ((key, value))) 27 | 28 | def delete(key: String): Self = 29 | create(asVector.filter(_._1 != key)) 30 | 31 | final def normalised: Self2 = 32 | if (isNormalised || asVector.isEmpty) 33 | this 34 | else 35 | normalise 36 | 37 | private lazy val normalise: Self2 = { 38 | // According to the Scala doc, this is a stable sort 39 | val result = asVector.sortBy(_._1) 40 | create(result, isNormalised = true) 41 | } 42 | 43 | override def toString = 44 | asVector 45 | .iterator 46 | .map(x => s"${x._1} -> ${x._2}") 47 | .mkString(displayName + "(", ", ", ")") 48 | 49 | override def hashCode = 50 | normalised.asVector.hashCode 51 | 52 | override def equals(that: Any) = 53 | that match { 54 | case t: AbstractMultiStringMap[_] => normalised.asVector == t.normalised.asVector 55 | case _ => false 56 | } 57 | 58 | def filterKeys(retain: String => Boolean): Self = { 59 | val vec2 = asVector.filter(kv => retain(kv._1)) 60 | if (vec2.length == asVector.length) this else create(vec2) 61 | } 62 | 63 | def whitelistKeys(whitelist: Set[String]): Self = 64 | filterKeys(whitelist.contains) 65 | 66 | def whitelistKeys(subset: Self2): Self = { 67 | val keys = subset.asVector.iterator.map(_._1).toSet 68 | whitelistKeys(keys) 69 | } 70 | 71 | def toMultiStringMap: MultiStringMap = 72 | new MultiStringMap(asVector, isNormalised = isNormalised) 73 | } 74 | 75 | object AbstractMultiStringMap { 76 | 77 | trait Module[A] { 78 | 79 | def fromVector(v: Vector[(String, String)]): A 80 | 81 | final val empty: A = 82 | fromVector(Vector.empty) 83 | 84 | final def apply(kvs: (String, String)*): A = 85 | fromVector(kvs.toVector) 86 | 87 | final def fromSeq(s: Seq[(String, String)]): A = 88 | fromVector(s.toVector) 89 | 90 | final def fromMap(m: Map[String, String]): A = 91 | fromVector(m.toVector) 92 | 93 | final def fromMultimap[C <: Iterable[String]](m: Map[String, C]): A = 94 | fromVector( 95 | m 96 | .iterator 97 | .flatMap { case (k, vs) => vs.iterator.map((k, _)) } 98 | .toVector 99 | ) 100 | 101 | final def fromJavaMultimap[C <: java.util.Collection[String]](multimap: java.util.Map[String, C]): A = 102 | fromVector( 103 | multimap 104 | .entrySet() 105 | .stream() 106 | .iterator() 107 | .asScala 108 | .flatMap(e => e.getValue.iterator().asScala.map(e.getKey -> _)) 109 | .toVector, 110 | ) 111 | 112 | final implicit def univEq: UnivEq[A] = 113 | UnivEq.force 114 | } 115 | 116 | } -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/general/Effect.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import scala.util.Try 4 | 5 | trait Effect[F[_]] extends Effect.Monad[F] { 6 | def bracket[A, B](fa: F[A])(use: A => F[B])(release: A => F[Unit]): F[B] 7 | } 8 | 9 | object Effect extends Effect_PlatformSpecific { 10 | 11 | trait Monad[F[_]] { 12 | def delay [A] (a: => A) : F[A] 13 | def pure [A] (a: A) : F[A] 14 | def map [A, B](fa: F[A])(f: A => B) : F[B] 15 | def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] 16 | 17 | def flatten[A](ffa: F[F[A]]): F[A] = 18 | flatMap(ffa)(identity) 19 | 20 | def suspend[A](fa: => F[A]): F[A] = 21 | flatMap(delay(fa))(identity) 22 | } 23 | 24 | trait Sync[F[_]] extends Effect[F] { 25 | def runSync[A](fa: F[A]): A 26 | } 27 | 28 | trait Async[F[_]] extends Effect[F] { 29 | def async[A](f: (Try[A] => Unit) => Unit): F[A] 30 | 31 | def timeoutMs[A](ms: Long)(fa: F[A]): F[Option[A]] 32 | 33 | def timeoutMsOrThrow[A](ms: Long, err: => Throwable)(fa: F[A]): F[A] = 34 | map( 35 | timeoutMs(ms)(fa) 36 | )(_.getOrElse(throw err)) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/general/Enabled.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.microlibs.utils.SafeBool 4 | 5 | sealed trait Enabled extends SafeBool.WithBoolOps[Enabled] { 6 | override final def companion = Enabled 7 | } 8 | 9 | case object Enabled extends Enabled with SafeBool.Object[Enabled] { 10 | override def positive = Enabled 11 | override def negative = Disabled 12 | } 13 | 14 | case object Disabled extends Enabled 15 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/general/ErrorMsg.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.univeq.UnivEq 4 | 5 | final case class ErrorMsg(value: String) { 6 | 7 | // Keep this as a val so that the stack trace points to where the error was created, as opposed to thrown. 8 | val exception: ErrorMsg.Exception = 9 | ErrorMsg.Exception(this) 10 | 11 | def throwException(): Nothing = 12 | throw exception 13 | 14 | def modMsg(f: String => String): ErrorMsg = { 15 | val e = ErrorMsg(f(value)) 16 | e.exception.setStackTrace(exception.getStackTrace) 17 | e 18 | } 19 | 20 | def withPrefix(s: String): ErrorMsg = 21 | modMsg(s + _) 22 | } 23 | 24 | object ErrorMsg { 25 | 26 | implicit def univEq: UnivEq[ErrorMsg] = 27 | UnivEq.derive 28 | 29 | def fromThrowable(t: Throwable): ErrorMsg = 30 | apply(Option(t.getMessage).getOrElse(t.toString).trim) 31 | 32 | def errorOccurred(t: Throwable): ErrorMsg = 33 | Option(t.getMessage).map(_.trim).filter(_.nonEmpty) match { 34 | case Some(m) => ErrorMsg("Error occurred: " + m) 35 | case None => ErrorMsg("Error occurred.") 36 | } 37 | 38 | object ClientSide { 39 | def errorContactingServer = ErrorMsg("Error contacting server. Please try again.") 40 | def failedToParseResponse = ErrorMsg("Failed to understand the response from the server.") 41 | def noCompatibleServer = ErrorMsg("Failed to find a compatible server. Please try again, or try reloading the page.") 42 | def serverCallTimeout = ErrorMsg("Server didn't respond. Please check your internet connectivity.") 43 | def serverProtocolIsNewer = ErrorMsg("Our servers have been upgraded to a newer version. Please reload this page and try again.") 44 | } 45 | 46 | final case class Exception(msg: ErrorMsg) extends RuntimeException(msg.value) 47 | } 48 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/general/LazyVal.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.univeq.UnivEq 4 | 5 | trait LazyVal[+A] { 6 | def value: A 7 | def valueThreadSafe: A 8 | 9 | override def equals(x: Any) = 10 | x match { 11 | case l: LazyVal[Any] => valueThreadSafe == l.valueThreadSafe 12 | case _ => false 13 | } 14 | } 15 | 16 | object LazyVal { 17 | 18 | def apply[A](a: => A): LazyVal[A] = 19 | new Lazy(() => a) 20 | 21 | private final class Lazy[+A](initArg: () => A) extends LazyVal[A] { 22 | 23 | // Don't prevent GC of initArg or waste mem propagating the ref 24 | private[this] var init = initArg 25 | 26 | private[this] var result: A = _ 27 | 28 | // Thread-unsafe 29 | override def value: A = { 30 | if (init ne null) { 31 | try 32 | result = init() 33 | catch { 34 | case t: Throwable => 35 | init = () => throw t 36 | throw t 37 | } 38 | init = null 39 | } 40 | result 41 | } 42 | 43 | override def valueThreadSafe: A = 44 | synchronized(value) 45 | } 46 | 47 | def pure[A](a: A): LazyVal[A] = 48 | new Pure(a) 49 | 50 | private final class Pure[+A](val value: A) extends LazyVal[A] { 51 | override def valueThreadSafe = value 52 | } 53 | 54 | implicit def univEq[A]: UnivEq[LazyVal[A]] = UnivEq.force 55 | } 56 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/general/MultiStringMap.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | /** Equivalent to a `Map[String, List[String]]`. */ 4 | final class MultiStringMap(asVector: Vector[(String, String)], isNormalised: Boolean) 5 | extends AbstractMultiStringMap[MultiStringMap](asVector, isNormalised) { 6 | 7 | override protected def create(asVector: Vector[(String, String)], isNormalised: Boolean = false) = 8 | new MultiStringMap(asVector, isNormalised) 9 | 10 | override def toMultiStringMap: MultiStringMap = 11 | this 12 | } 13 | 14 | object MultiStringMap extends AbstractMultiStringMap.Module[MultiStringMap] { 15 | 16 | override def fromVector(v: Vector[(String, String)]): MultiStringMap = 17 | new MultiStringMap(v, isNormalised = false) 18 | } 19 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/general/Permission.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.microlibs.utils.SafeBool 4 | 5 | /** Type-safe union of `Allow | Deny` */ 6 | sealed abstract class Permission extends SafeBool.WithBoolOps[Permission] { 7 | override final def companion = Permission 8 | 9 | def apply[A](a: => A): Permission.DeniedOr[A] 10 | def option[A](a: => A): Option[A] 11 | } 12 | 13 | case object Allow extends Permission { 14 | override def apply[A](a: => A) = Right(a) 15 | override def option[A](a: => A) = Some(a) 16 | } 17 | 18 | case object Deny extends Permission { 19 | override def apply[A](a: => A) = Permission.denied 20 | override def option[A](a: => A) = None 21 | } 22 | 23 | object Permission extends SafeBool.Object[Permission] { 24 | override def positive = Allow 25 | override def negative = Deny 26 | 27 | type DeniedOr[+A] = Either[Deny.type, A] 28 | val denied: Permission.DeniedOr[Nothing] = Left(Deny) 29 | } 30 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/general/Protocol.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | /** Uni-directional protocol. */ 4 | trait Protocol[F[_]] { self => 5 | type Type 6 | val codec: F[Type] 7 | 8 | final type AndValue = Protocol.AndValue[F] { type Type = self.Type } 9 | 10 | final def andValue(v: Type): AndValue = 11 | new Protocol.AndValue[F] { 12 | override type Type = self.Type 13 | override val codec = self.codec 14 | override val value = v 15 | } 16 | } 17 | 18 | object Protocol { 19 | 20 | type Of[F[_], A] = Protocol[F] { type Type = A } 21 | 22 | def apply[F[_], A](c: F[A]): Of[F, A] = 23 | new Protocol[F] { 24 | override type Type = A 25 | override val codec = c 26 | } 27 | 28 | trait AndValue[F[_]] { 29 | type Type 30 | val codec: F[Type] 31 | val value: Type 32 | 33 | override def toString = s"Protocol.AndValue($value)" 34 | 35 | def unsafeForceType[A]: AndValue.Of[F, A] = 36 | this.asInstanceOf[AndValue.Of[F, A]] 37 | } 38 | 39 | object AndValue { 40 | type Of[F[_], A] = AndValue[F] { type Type = A } 41 | } 42 | 43 | // =================================================================================================================== 44 | 45 | /** Polymorphic bi-directional protocol. 46 | * 47 | * This is polymorphic in the sense that the response protocol can vary based on the runtime value of the request. 48 | * By calling `prepareSend` with a request value, one can get back the appropriate, associated response protocol. 49 | */ 50 | trait RequestResponse[F[_]] { 51 | type RequestType 52 | type ResponseType 53 | 54 | final type PreparedSend = RequestResponse.PreparedSend.Of[F, PreparedRequestType, ResponseType] 55 | type PreparedRequestType 56 | def prepareSend(r: RequestType): PreparedSend 57 | } 58 | 59 | object RequestResponse { 60 | 61 | /** Monomorphic bi-directional protocol. */ 62 | type Simple[F[_], Req, Res] = RequestResponse[F] { 63 | type RequestType = Req 64 | type ResponseType = Res 65 | type PreparedRequestType = Req 66 | } 67 | 68 | /** Monomorphic bi-directional protocol. */ 69 | def simple[F[_], Req, Res](res: Protocol.Of[F, Res]): Simple[F, Req, Res] = 70 | new RequestResponse[F] { 71 | override type RequestType = Req 72 | override type ResponseType = Res 73 | override type PreparedRequestType = Req 74 | override def prepareSend(r: Req) = PreparedSend(r, res) 75 | } 76 | 77 | trait PreparedSend[F[_], Req] { 78 | val request : Req 79 | val response: Protocol[F] 80 | } 81 | 82 | object PreparedSend { 83 | type Of[F[_], Req, Res] = PreparedSend[F, Req] { 84 | val request : Req 85 | val response: Protocol.Of[F, Res] 86 | } 87 | 88 | def apply[F[_], Req, Res](req: Req, res: Protocol.Of[F, Res]): Of[F, Req, Res] = 89 | new PreparedSend[F, Req] { 90 | override val request = req 91 | override val response: Protocol.Of[F, Res] = res 92 | } 93 | } 94 | } 95 | 96 | // =================================================================================================================== 97 | 98 | object WebSocket { 99 | 100 | /** Client can send requests (ReqRes) 101 | * Server can send messages (Push) 102 | */ 103 | trait ClientReqServerPush[F[_]] { 104 | type ReqId 105 | type ReqRes <: Protocol.RequestResponse[F] { type PreparedRequestType = Req } 106 | final type Req = req.Type 107 | final type Push = push.Type 108 | val url: Url.Relative 109 | val req: Protocol[F] 110 | val push: Protocol[F] 111 | } 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/general/Retries.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.microlibs.stdlib_ext.StdlibExt._ 4 | import java.time.Duration 5 | import scala.collection.View 6 | 7 | /** Immutable retry policy */ 8 | final case class Retries(waitTimes: Iterable[Duration]) { 9 | 10 | def apply(attemptsSoFar: Int): Option[Duration] = 11 | waitTimes.drop(attemptsSoFar).headOption 12 | 13 | def isEmpty: Boolean = 14 | waitTimes.isEmpty 15 | 16 | def take(n: Int): Retries = 17 | Retries(waitTimes.take(n)) 18 | 19 | def takeWhile(f: Duration => Boolean): Retries = 20 | Retries(waitTimes.takeWhile(f)) 21 | 22 | def pop: Option[(Duration, Retries)] = 23 | if (isEmpty) 24 | None 25 | else 26 | Some((waitTimes.head, Retries(waitTimes.tail))) 27 | 28 | def ++(r: Retries): Retries = 29 | Retries(waitTimes ++ r.waitTimes) 30 | } 31 | 32 | object Retries { 33 | private def expStream(d: Duration, factor: Double): LazyList[Duration] = 34 | d #:: expStream((d.toMillis * factor).millis, factor) 35 | 36 | def exponentially(d: Duration, factor: Double = 2): Retries = 37 | apply(expStream(d, factor)) 38 | 39 | def continually(d: Duration): Retries = 40 | apply(View.from(Iterator.continually(d))) 41 | 42 | def none: Retries = 43 | Retries(Nil) 44 | } 45 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/general/Version.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.general 2 | 3 | import japgolly.microlibs.utils.Memo 4 | import japgolly.univeq.UnivEq 5 | 6 | final case class Version(major: Version.Major, minor: Version.Minor) { 7 | override def toString = verStr 8 | def verNum = s"${major.value}.${minor.value}" 9 | def verStr = "v" + verNum 10 | } 11 | 12 | object Version { 13 | 14 | def fromInts(major: Int, minor: Int): Version = 15 | Version(Major(major), Minor(minor)) 16 | 17 | final case class Major(value: Int) { 18 | assert(value >= 1) 19 | } 20 | 21 | final case class Minor(value: Int) { 22 | assert(value >= 0) 23 | } 24 | 25 | implicit def univEqMajor: UnivEq[Major] = UnivEq.derive 26 | implicit def univEqMinor: UnivEq[Minor] = UnivEq.derive 27 | implicit def univEq : UnivEq[Version] = UnivEq.derive 28 | 29 | implicit val ordering: Ordering[Version] = 30 | new Ordering[Version] { 31 | override def compare(x: Version, y: Version): Int = { 32 | val i = x.major.value - y.major.value 33 | if (i != 0) 34 | i 35 | else 36 | x.minor.value - y.minor.value 37 | } 38 | } 39 | 40 | private val memoV1: Int => Version = Memo.int(minorVer => Version.fromInts(1, minorVer)) 41 | def v1(minorVer: Int): Version = memoV1(minorVer) 42 | 43 | private val memoV2: Int => Version = Memo.int(minorVer => Version.fromInts(2, minorVer)) 44 | def v2(minorVer: Int): Version = memoV2(minorVer) 45 | } 46 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/http/Cookie.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.http 2 | 3 | import japgolly.univeq._ 4 | 5 | final case class Cookie(name : Cookie.Name, 6 | value : String, 7 | maxAgeInSec: Option[Int], 8 | httpOnly : Option[Boolean], 9 | secure : Option[Boolean]) 10 | 11 | object Cookie { 12 | final case class Name(value: String) 13 | 14 | type LookupFn = Name => Option[String] 15 | 16 | object LookupFn { 17 | 18 | val empty: LookupFn = 19 | _ => None 20 | 21 | def overHeader(header: String): Cookie.LookupFn = 22 | if (header eq null) 23 | empty 24 | else 25 | name => { 26 | val k = name.value + "=" 27 | 28 | val startIdx: Int = 29 | if (header.startsWith(k)) 30 | 0 31 | else { 32 | val i = header.indexOf("; " + k, k.length) 33 | if (i > 0) i + 2 else -1 34 | } 35 | 36 | if (startIdx < 0) 37 | None 38 | else 39 | Some(header.substring(startIdx + k.length).takeWhile(_ != ';')) 40 | } 41 | } 42 | 43 | final case class Update(add: List[Cookie], remove: List[Cookie.Name]) 44 | 45 | object Update { 46 | val empty = apply(Nil, Nil) 47 | def add(c: Cookie) = apply(c :: Nil, Nil) 48 | } 49 | 50 | implicit def univEqName : UnivEq[Name] = UnivEq.derive 51 | implicit def univEqCookie: UnivEq[Cookie] = UnivEq.derive 52 | implicit def univEqUpdate: UnivEq[Update] = UnivEq.derive 53 | } 54 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/japgolly/webapputil/http/UrlEncoderApi.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.http 2 | 3 | trait UrlEncoderApi { 4 | def encode(str: String): String 5 | def decode(str: String): String 6 | } 7 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/japgolly/webapputil/http/HttpClientTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.http 2 | 3 | import japgolly.microlibs.testutil.TestUtil._ 4 | import nyaya.gen._ 5 | import utest._ 6 | 7 | object HttpClientTest extends TestSuite { 8 | import HttpClient._ 9 | 10 | val genUriParams: Gen[UriParams] = { 11 | val char = Gen.chooseGen(Gen.char, Gen.alphaNumeric, Gen.chooseChar('=', "&?+ ")) 12 | val genKey = char.string(1 to 4) 13 | val genStr = char.string(0 to 8) 14 | val genVal = Gen.chooseGen(genStr, genStr, genStr, Gen.pure[String](null)) 15 | (genKey & genVal).list(0 to 8).map(UriParams.fromSeq) 16 | } 17 | 18 | override def tests = Tests { 19 | 20 | "uriParams" - { 21 | "spot" - { 22 | val src = UriParams( 23 | "a" -> "1", 24 | "c" -> "x = & + x", 25 | "n" -> null, 26 | "a" -> "2", 27 | ) 28 | val str = src.asString 29 | assertEq(str, "a=1&c=x+%3D+%26+%2B+x&n&a=2") 30 | 31 | val ps2 = UriParams.parse(str) 32 | assertEq(ps2.asVector, src.asVector) 33 | } 34 | 35 | "roundTrip" - { 36 | for (src <- genUriParams.samples().take(80)) { 37 | val str = src.asString 38 | val ps2 = UriParams.parse(str) 39 | // println() 40 | // println(src.asVector) 41 | // println("\"" + str + "\"") 42 | // assertEq(str, ps2.asVector, src.asVector) 43 | assertSeq(str, ps2.asVector, src.asVector) 44 | } 45 | } 46 | 47 | "normalisation" - { 48 | val src = UriParams("b" -> "2", "z" -> "1", "b" -> "0", "a" -> "6") 49 | val tgt = UriParams("a" -> "6", "b" -> "2", "b" -> "0", "z" -> "1") 50 | val bad = UriParams("b" -> "0", "z" -> "1", "b" -> "2", "a" -> "6") 51 | assertEq(src.normalised.asVector, tgt.asVector) 52 | assertEq(src, tgt) 53 | assertNotEq(src, bad) 54 | assertNotEq(tgt, bad) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /coreBoopickle/js/src/main/scala/japgolly/webapputil/boopickle/BinaryFormatExt.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle 2 | 3 | import boopickle.{PickleImpl, Pickler, UnpickleImpl} 4 | import japgolly.scalajs.react.callback.AsyncCallback 5 | import japgolly.webapputil.binary._ 6 | import java.nio.ByteBuffer 7 | import scala.scalajs.js 8 | 9 | object BinaryFormatExt { 10 | 11 | trait Implicits { 12 | 13 | @inline final implicit def BinaryFormatBoopickleExt[A](self: BinaryFormat[A]): Implicits.BinaryFormatBoopickleExt[A] = 14 | new Implicits.BinaryFormatBoopickleExt[A](self) 15 | 16 | @inline final implicit def BinaryFormatBoopickleStaticExt(self: BinaryFormat.type): Implicits.BinaryFormatBoopickleStaticExt = 17 | new Implicits.BinaryFormatBoopickleStaticExt(self) 18 | } 19 | 20 | object Implicits extends Implicits { 21 | 22 | final class BinaryFormatBoopickleExt[A](private val self: BinaryFormat[A]) extends AnyVal { 23 | type ThisIsBinary = BinaryFormat[A] =:= BinaryFormat[BinaryData] 24 | 25 | def pickle[B](implicit pickler: SafePickler[B], ev: ThisIsBinary): BinaryFormat[B] = 26 | ev(self).xmap(pickler.decodeOrThrow)(pickler.encode) 27 | 28 | def pickleBasic[B](implicit pickler: Pickler[B], ev: ThisIsBinary): BinaryFormat[B] = { 29 | val unpickle = UnpickleImpl[B] 30 | ev(self) 31 | .xmap[ByteBuffer](_.unsafeByteBuffer)(BinaryData.unsafeFromByteBuffer) 32 | .xmap(unpickle.fromBytes(_))(PickleImpl.intoBytes(_)) 33 | } 34 | } 35 | 36 | final class BinaryFormatBoopickleStaticExt(private val self: BinaryFormat.type) extends AnyVal { 37 | @inline def pickleCompressEncrypt[A](c: Compression, e: Encryption)(implicit pickler: SafePickler[A]): BinaryFormat[A] = 38 | BinaryFormatExt.pickleCompressEncrypt(c, e) 39 | 40 | @inline def versioned[A](oldest: BinaryFormat[A], toLatest: BinaryFormat[A]*): BinaryFormat[A] = 41 | BinaryFormatExt.versioned(oldest, toLatest: _*) 42 | } 43 | } 44 | 45 | // =================================================================================================================== 46 | 47 | def versioned[A](oldest: BinaryFormat[A], toLatest: BinaryFormat[A]*): BinaryFormat[A] = { 48 | val layers = oldest +: toLatest.toArray 49 | val decoders = layers 50 | val decoderIndices = decoders.indices 51 | val latestVer = decoders.length - 1 52 | val latestVerHeader = BinaryData.byte(latestVer.toByte) 53 | val encoder = layers.last 54 | 55 | def encode(a: A): AsyncCallback[BinaryData] = 56 | encoder.encode(a).map(latestVerHeader ++ _) 57 | 58 | def decode(bin: BinaryData): AsyncCallback[A] = 59 | AsyncCallback.suspend { 60 | 61 | if (bin.isEmpty) 62 | throw js.JavaScriptException("No data") 63 | 64 | val ver = bin.unsafeArray(0).toInt 65 | 66 | if (decoderIndices.contains(ver)) { 67 | val binBody = bin.drop(1) 68 | decoders(ver).decode(binBody) 69 | } else if (ver < 0) 70 | throw js.JavaScriptException("Bad data") 71 | else 72 | SafePicklerUtil.unsupportedVer(ver, latestVer) 73 | } 74 | 75 | BinaryFormat.async(decode)(encode) 76 | } 77 | 78 | def pickleCompressEncrypt[A](c: Compression, e: Encryption)(implicit pickler: SafePickler[A]): BinaryFormat[A] = 79 | BinaryFormat.id 80 | .encrypt(e) // 3. Encryption is the very last step 81 | .compress(c) // 2. Compress the binary *before* encrypting 82 | .pickle[A] // 1. Generate binary first 83 | 84 | } 85 | -------------------------------------------------------------------------------- /coreBoopickle/js/src/main/scala/japgolly/webapputil/boopickle/BinaryWebWorkerProtocol.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle 2 | 3 | import boopickle.{PickleImpl, Pickler, UnpickleImpl} 4 | import japgolly.webapputil.webworker.WebWorkerProtocol 5 | import org.scalajs.dom.Transferable 6 | import scala.scalajs.js 7 | import scala.scalajs.js.typedarray.TypedArrayBufferOps._ 8 | import scala.scalajs.js.typedarray._ 9 | 10 | object BinaryWebWorkerProtocol extends WebWorkerProtocol { 11 | override type Encoded = ArrayBuffer 12 | override type Decoder[A] = Pickler[A] 13 | override type Encoder[A] = Pickler[A] 14 | 15 | override def encode[A: Pickler](input: A): ArrayBuffer = { 16 | val bb = PickleImpl.intoBytes(input) 17 | val len = bb.limit() 18 | val ia = bb.typedArray().subarray(0, len) 19 | val ab = ia.buffer.slice(0, len) 20 | ab 21 | } 22 | 23 | override def decode[A: Pickler](encoded: ArrayBuffer): A = { 24 | val bb = TypedArrayBuffer wrap encoded 25 | UnpickleImpl[A].fromBytes(bb) 26 | } 27 | 28 | // https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast 29 | override def transferables(e: ArrayBuffer): js.UndefOr[js.Array[Transferable]] = 30 | js.Array(e: Transferable) 31 | } 32 | -------------------------------------------------------------------------------- /coreBoopickle/js/src/main/scala/japgolly/webapputil/boopickle/IndexedDbExt.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle 2 | 3 | import japgolly.webapputil.binary._ 4 | import japgolly.webapputil.indexeddb._ 5 | 6 | object IndexedDbExt { 7 | 8 | object Implicits { 9 | @inline implicit final class ValueCodecBoopickleExt[A](private val self: ValueCodec[A]) extends AnyVal { 10 | 11 | type ThisIsBinary = ValueCodec[A] =:= ValueCodec[BinaryData] 12 | 13 | def pickle[B](implicit pickler: SafePickler[B], ev: ThisIsBinary): ValueCodec[B] = 14 | ev(self).xmap(pickler.decodeOrThrow)(pickler.encode) 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /coreBoopickle/js/src/test/scala/japgolly/webapputil/boopickle/BinaryFormatTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle 2 | 3 | import boopickle.DefaultBasic._ 4 | import japgolly.microlibs.testutil.TestUtil._ 5 | import japgolly.webapputil.binary._ 6 | import japgolly.webapputil.test.node.TestNode.asyncTest 7 | import utest._ 8 | 9 | object BinaryFormatTest extends TestSuite { 10 | 11 | override def tests = Tests { 12 | 13 | // Note: pickleCompressEncrypt is covered in IndexedDbTest 14 | 15 | "versionedBinary" - asyncTest() { 16 | type A = Int 17 | 18 | val codec1: BinaryFormat[A] = BinaryFormat.id.pickleBasic[Int] 19 | val codec2: BinaryFormat[A] = BinaryFormat.id.pickleBasic[String].xmap(_.toInt)(_.toString) 20 | 21 | val v1 = BinaryFormatExt.versioned(codec1) 22 | val v2 = BinaryFormatExt.versioned(codec1, codec2) 23 | 24 | for { 25 | bin1 <- v1.encode(123) 26 | bin2 <- v2.encode(687) 27 | res1v1 <- v1.decode(bin1) 28 | res1v2 <- v2.decode(bin1) 29 | res2v1 <- v1.decode(bin2).attempt 30 | res2v2 <- v2.decode(bin2) 31 | res0 <- v2.decode(BinaryData.empty).attempt 32 | } yield { 33 | 34 | assertEq(res1v1, 123) 35 | assertEq(res1v2, 123) 36 | assertEq(res2v2, 687) 37 | 38 | assert(res2v1.isLeft) 39 | assert(res0.isLeft) 40 | 41 | s"""bin1 = $bin1 42 | |bin2 = $bin2 43 | |res2v1 = $res2v1 44 | |res0 = $res0 45 | |""".stripMargin.trim 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /coreBoopickle/js/src/test/scala/japgolly/webapputil/boopickle/BinaryStringTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle 2 | 3 | import japgolly.microlibs.testutil.TestUtil._ 4 | import japgolly.webapputil.binary._ 5 | import nyaya.gen.Gen 6 | import sourcecode.Line 7 | import utest._ 8 | import utest.framework.TestPath 9 | 10 | object BinaryStringTest extends TestSuite { 11 | 12 | private implicit val encoder: BinaryString.Encoder = 13 | BinaryString.Base32768.global 14 | 15 | private def assertRoundTrip(hex: String)(implicit l: Line): Any = 16 | assertRoundTrip(BinaryData.fromHex(hex)) 17 | 18 | private def assertRoundTrip(bin: BinaryData)(implicit l: Line): Any = { 19 | val bs = BinaryString(bin) 20 | val desc = BinaryData.fromStringAsUtf8(bs.encoded).describe() 21 | assertEq(desc, bs.binaryValue, bin) 22 | desc 23 | } 24 | 25 | override def tests = Tests { 26 | 27 | "roundTrip" - { 28 | def roundTripTest()(implicit tp: TestPath, l: Line) = 29 | assertRoundTrip(tp.value.last) 30 | 31 | "" - roundTripTest() 32 | 33 | "00" - roundTripTest() 34 | "ff" - roundTripTest() 35 | "ff00" - roundTripTest() 36 | "00ff" - roundTripTest() 37 | "ff00ff" - roundTripTest() 38 | "00ff00" - roundTripTest() 39 | "00ff00ff" - roundTripTest() 40 | "ff00ff00" - roundTripTest() 41 | 42 | "0011" - roundTripTest() 43 | "001122" - roundTripTest() 44 | "00112233" - roundTripTest() 45 | "0011223344" - roundTripTest() 46 | "001122334455" - roundTripTest() 47 | "00112233445566" - roundTripTest() 48 | "0011223344556677" - roundTripTest() 49 | "001122334455667788" - roundTripTest() 50 | "00112233445566778899" - roundTripTest() 51 | "00112233445566778899aa" - roundTripTest() 52 | "00112233445566778899aabb" - roundTripTest() 53 | "00112233445566778899aabbcc" - roundTripTest() 54 | "00112233445566778899aabbccdd" - roundTripTest() 55 | "00112233445566778899aabbccddee" - roundTripTest() 56 | "00112233445566778899aabbccddeeff" - roundTripTest() 57 | 58 | "1100" - roundTripTest() 59 | "112200" - roundTripTest() 60 | "11223300" - roundTripTest() 61 | "1122334400" - roundTripTest() 62 | "112233445500" - roundTripTest() 63 | "11223344556600" - roundTripTest() 64 | "1122334455667700" - roundTripTest() 65 | "112233445566778800" - roundTripTest() 66 | "11223344556677889900" - roundTripTest() 67 | "112233445566778899aa00" - roundTripTest() 68 | "112233445566778899aabb00" - roundTripTest() 69 | "112233445566778899aabbcc00" - roundTripTest() 70 | "112233445566778899aabbccdd00" - roundTripTest() 71 | "112233445566778899aabbccddee00" - roundTripTest() 72 | "112233445566778899aabbccddeeff00" - roundTripTest() 73 | 74 | "random" - { 75 | for (a <- Gen.byte.arraySeq(1 to 65536 * 2).samples().take(100)) 76 | assertRoundTrip(BinaryData.fromArraySeq(a)) 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /coreBoopickle/js/src/test/scala/japgolly/webapputil/boopickle/CompressionTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle 2 | 3 | import japgolly.microlibs.testutil.TestUtil._ 4 | import japgolly.webapputil.binary._ 5 | import nyaya.gen.Gen 6 | import sourcecode.Line 7 | import utest._ 8 | 9 | object CompressionTest extends TestSuite { 10 | 11 | private implicit val pako: Pako = 12 | Pako.global 13 | 14 | private def assertRoundTrip(zip: Compression, dataSize: Int)(implicit l: Line): String = { 15 | val src = BinaryData.fromArraySeq(Gen.byte.arraySeq(dataSize).sample()) 16 | assertRoundTrip(zip, src) 17 | } 18 | 19 | private def assertRoundTrip(zip: Compression, src: BinaryData)(implicit l: Line): String = { 20 | val zipped = zip.compress(src.duplicate) 21 | val unzipped = zip.decompressOrThrow(zipped) 22 | assertEq(actual = unzipped, expect = src) 23 | assertNotEq(src, zipped) 24 | "%,d bytes => %,d bytes".format(src.length, zipped.length) 25 | } 26 | 27 | override def tests = Tests { 28 | "9_raw_0" - assertRoundTrip(Compression.ViaPako(9, false), 0) 29 | "9_raw_41" - assertRoundTrip(Compression.ViaPako(9, false), 41) 30 | "9_raw_12345" - assertRoundTrip(Compression.ViaPako(9, false), 12345) 31 | "3_raw_800" - assertRoundTrip(Compression.ViaPako(3, false), 800) 32 | "9_hdr_1771" - assertRoundTrip(Compression.ViaPako(9, true), 1771) 33 | "9_hdr_bin" - assertRoundTrip(Compression.ViaPako(9, true), BinaryData.fromStringAsUtf8("321654" * 987)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /coreBoopickle/jvm/src/main/scala/japgolly/webapputil/boopickle/BinaryFormatExt.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle 2 | 3 | object BinaryFormatExt { 4 | trait Implicits 5 | } 6 | -------------------------------------------------------------------------------- /coreBoopickle/shared/src/main/scala/japgolly/webapputil/boopickle/BoopickleCodecEngine.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle 2 | 3 | import boopickle.{PickleImpl, Pickler, UnpickleImpl} 4 | import japgolly.webapputil.binary._ 5 | 6 | object BoopickleCodecEngine { 7 | 8 | object pickler extends CodecEngine[Pickler, Throwable] { 9 | 10 | override def encode[A](a: A)(implicit p: Pickler[A]): BinaryData = { 11 | val bb = PickleImpl.intoBytes(a) 12 | BinaryData.unsafeFromByteBuffer(bb) 13 | } 14 | 15 | override def decode[A](b: BinaryData)(implicit p: Pickler[A]): Either[Throwable, A] = 16 | try { 17 | val a = UnpickleImpl(p).fromBytes(b.unsafeByteBuffer) 18 | Right(a) 19 | } catch { 20 | case t: Throwable => 21 | Left(t) 22 | } 23 | } 24 | 25 | object safePickler extends CodecEngine[SafePickler, SafePickler.DecodingFailure] { 26 | override def encode[A](a: A)(implicit p: SafePickler[A]): BinaryData = 27 | p.encode(a) 28 | 29 | override def decode[A](b: BinaryData)(implicit p: SafePickler[A]): SafePickler.Result[A] = 30 | p.decode(b) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /coreBoopickle/shared/src/main/scala/japgolly/webapputil/boopickle/EntrypointDefExt.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle 2 | 3 | import boopickle._ 4 | import japgolly.webapputil.binary._ 5 | import japgolly.webapputil.entrypoint._ 6 | import java.nio.ByteBuffer 7 | 8 | object EntrypointDefExt { 9 | import EntrypointDef.Codec 10 | 11 | trait Implicits { 12 | @inline final implicit def EntrypointDefCodecBoopickleExt[A](self: Codec[A]): Implicits.EntrypointDefCodecBoopickleExt[A] = 13 | new Implicits.EntrypointDefCodecBoopickleExt[A](self) 14 | 15 | @inline final implicit def implicitEntrypointDefCodecViaBoopickle[A](implicit p: Pickler[A]): EntrypointDef.Codec[A] = 16 | EntrypointDef.Codec.binary.pickle[A] 17 | } 18 | 19 | object Implicits extends Implicits { 20 | final class EntrypointDefCodecBoopickleExt[A](private val self: Codec[A]) extends AnyVal { 21 | type ThisIsBinary = Codec[A] =:= Codec[BinaryData] 22 | 23 | def pickle[B](implicit pickler: Pickler[B], ev: ThisIsBinary): Codec[B] = { 24 | val unpickle = UnpickleImpl[B] 25 | ev(self) 26 | .xmap[ByteBuffer](_.unsafeByteBuffer)(BinaryData.unsafeFromByteBuffer) 27 | .xmap(unpickle.fromBytes(_))(PickleImpl.intoBytes(_)) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /coreBoopickle/shared/src/main/scala/japgolly/webapputil/boopickle/SafePicklerUtil.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle 2 | 3 | import boopickle.{PickleState, UnpickleState} 4 | import japgolly.webapputil.general.Version 5 | 6 | object SafePicklerUtil { 7 | import PicklerUtil._ 8 | 9 | final case class UnsupportedVersionException(found: Version, maxSupported: Version) 10 | extends RuntimeException(s"${found.verStr} not supported. ${maxSupported.verStr} is the max supported.") 11 | 12 | case object CorruptData 13 | extends RuntimeException("Corrupt data.") 14 | 15 | /** Used to add a codec version to a binary protocol whilst retaining backwards-compatibility with the unversioned 16 | * case. 17 | */ 18 | final val VersionHeader = -99988999 19 | 20 | def writeVersion(ver: Int)(implicit state: PickleState): Unit = { 21 | assert(ver > 0) // v1.0 is the default and doesn't need a version header 22 | state.enc.writeInt(VersionHeader) 23 | state.enc.writeInt(ver) 24 | } 25 | 26 | def unsupportedVer(ver: Int, maxSupportedVer: Int): Nothing = 27 | throw UnsupportedVersionException(found = Version.v1(ver), maxSupported = Version.v1(maxSupportedVer)) 28 | 29 | def readByVersion[A](maxSupportedVer: Int)(f: PartialFunction[Int, A])(implicit state: UnpickleState): A = { 30 | assert(maxSupportedVer > 0) 31 | 32 | def unsupportedVer(ver: Int): Nothing = 33 | SafePicklerUtil.unsupportedVer(ver, maxSupportedVer) 34 | 35 | def readVer(ver: Int): A = 36 | f.applyOrElse[Int, A](ver, unsupportedVer) 37 | 38 | state.dec.peek(_.readInt) match { 39 | case VersionHeader => 40 | state.dec.readInt 41 | val ver = state.dec.readInt 42 | if (ver <= 0) 43 | throw CorruptData 44 | if (ver > maxSupportedVer) // preempt using the partial function in case maxSupportedVer is incorrect 45 | unsupportedVer(ver) 46 | readVer(ver) 47 | case _ => 48 | readVer(0) 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /coreBoopickle/shared/src/main/scala/japgolly/webapputil/boopickle/package.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil 2 | 3 | package object boopickle 4 | extends boopickle.BinaryFormatExt.Implicits 5 | with boopickle.EntrypointDefExt.Implicits 6 | -------------------------------------------------------------------------------- /coreCatsEffect/js/src/main/scala/japgolly/webapputil/cats/effect/PlatformImplicits.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.cats.effect 2 | 3 | trait PlatformImplicits 4 | -------------------------------------------------------------------------------- /coreCatsEffect/js/src/main/scala/japgolly/webapputil/cats/effect/WebappUtilEffectIO.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.cats.effect 2 | 3 | import cats.effect.IO 4 | import japgolly.webapputil.general.Effect 5 | 6 | object WebappUtilEffectIO extends WebappUtilEffectAsyncIO { 7 | 8 | trait Implicits { 9 | @inline final implicit def webappUtilEffectIO: Effect.Async[IO] = 10 | WebappUtilEffectIO 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /coreCatsEffect/jvm/src/main/scala/japgolly/webapputil/cats/effect/PlatformImplicits.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.cats.effect 2 | 3 | import cats.effect.IO 4 | 5 | trait PlatformImplicits { 6 | 7 | @inline final implicit def webappUtilIOExt[A](io: IO[A]): WebappUtilIOExt[A] = 8 | new WebappUtilIOExt(io) 9 | } 10 | -------------------------------------------------------------------------------- /coreCatsEffect/jvm/src/main/scala/japgolly/webapputil/cats/effect/ThreadUtilsIO.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.cats.effect 2 | 3 | import cats.effect.unsafe.{IORuntime, IORuntimeConfig} 4 | import cats.effect.{IO, Resource} 5 | import com.typesafe.scalalogging.Logger 6 | import japgolly.webapputil.general.ThreadUtils 7 | import java.time.Duration 8 | import java.util.concurrent.{Executors, TimeUnit} 9 | import scala.concurrent.ExecutionContext 10 | 11 | object ThreadUtilsIO { 12 | import ThreadUtils.{ThreadGroups, ThreadPool, ThreadPool2, newThreadPool} 13 | 14 | object Runtimes { 15 | lazy val scheduledTasks = newDefaultRuntime("ScheduledTasks") 16 | lazy val shutdown = newDefaultRuntime("Shutdown") 17 | } 18 | 19 | def newDefaultRuntime(threadPrefix: String): IORuntime = { 20 | import IORuntime._ 21 | val (compute, _) = createWorkStealingComputeThreadPool(threadPrefix = s"$threadPrefix-compute") 22 | val (blocking, _) = createDefaultBlockingExecutionContext(threadPrefix = s"$threadPrefix-blocking") 23 | val (scheduler, _) = createDefaultScheduler(threadPrefix = s"$threadPrefix-scheduler") 24 | IORuntime(compute, blocking, scheduler, () => (), IORuntimeConfig()) 25 | } 26 | 27 | def runOnShutdown(name: String, proc: => Unit): Unit = 28 | runOnShutdown(name, IO(proc)) 29 | 30 | def runOnShutdown(name: String, task: IO[Unit]): Unit = { 31 | val t = new Thread(ThreadGroups.shutdown, task.toJavaRunnable(Runtimes.shutdown), "shutdown-" + name) 32 | java.lang.Runtime.getRuntime.addShutdownHook(t) 33 | } 34 | 35 | // =================================================================================================================== 36 | 37 | def threadPool(threadGroupName: String, logger: Logger)(f: ThreadPool => ThreadPool2): Resource[IO, ExecutionContext] = 38 | Resource.make[IO, ThreadPool2]( 39 | IO(f(newThreadPool(threadGroupName, logger))) 40 | )( 41 | a => IO(a.threadPoolExecutor.shutdown()) 42 | ).map(_.executionContext) 43 | 44 | // =================================================================================================================== 45 | 46 | def newScheduler(threadName: String, threadGroup: ThreadGroup): Scheduler = 47 | new Scheduler(threadName, threadGroup) 48 | 49 | final class Scheduler(threadName: String, threadGroup: ThreadGroup) { 50 | 51 | val executorService = 52 | Executors.newSingleThreadScheduledExecutor(new Thread(threadGroup, _, threadName)) 53 | 54 | def scheduleAtFixedRate[A](io: IO[A], period: Duration): Scheduler = 55 | scheduleAtFixedRate(io, period, period) 56 | 57 | def scheduleAtFixedRate[A](io: IO[A], initialDelay: Duration, period: Duration): Scheduler = { 58 | executorService.scheduleAtFixedRate( 59 | io.toJavaRunnable(Runtimes.scheduledTasks), 60 | initialDelay.toMillis, 61 | period.toMillis, 62 | TimeUnit.MILLISECONDS) 63 | this 64 | } 65 | 66 | def addShutdownHook(io: IO[Unit]): Scheduler = { 67 | runOnShutdown(threadName, io) 68 | this 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /coreCatsEffect/jvm/src/main/scala/japgolly/webapputil/cats/effect/WebappUtilEffectIO.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.cats.effect 2 | 3 | import cats.effect.IO 4 | import cats.effect.unsafe.IORuntime 5 | import japgolly.webapputil.general.Effect 6 | 7 | object WebappUtilEffectIO { 8 | trait Implicits { 9 | @inline final implicit def webappUtilEffectIO(implicit r: IORuntime): Effect.Async[IO] with Effect.Sync[IO] = 10 | new WebappUtilEffectAsyncIO with Effect.Sync[IO] { 11 | override def runSync[A](fa: IO[A]): A = 12 | fa.unsafeRunSync() 13 | } 14 | } 15 | } 16 | 17 | // Scala 3 doesn't like this. Complains about an illegal cycle 18 | // class WebappUtilEffectIO()(implicit runtime: IORuntime) extends WebappUtilEffectAsyncIO with Effect.Sync[IO] { 19 | // override def runSync[A](fa: IO[A]): A = 20 | // fa.unsafeRunSync() 21 | // } 22 | -------------------------------------------------------------------------------- /coreCatsEffect/jvm/src/main/scala/japgolly/webapputil/cats/effect/WebappUtilIOExt.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.cats.effect 2 | 3 | import cats.effect.IO 4 | import cats.effect.unsafe.IORuntime 5 | 6 | class WebappUtilIOExt[A](private val self: IO[A]) extends AnyVal { 7 | 8 | def toJavaRunnable(implicit r: IORuntime): Runnable = 9 | () => { 10 | self.unsafeRunSync()(r) 11 | () 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /coreCatsEffect/shared/src/main/scala/japgolly/webapputil/cats/effect/Implicits.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.cats.effect 2 | 3 | object Implicits extends Implicits 4 | 5 | trait Implicits 6 | extends PlatformImplicits 7 | with WebappUtilEffectIO.Implicits 8 | -------------------------------------------------------------------------------- /coreCatsEffect/shared/src/main/scala/japgolly/webapputil/cats/effect/WebappUtilEffectAsyncIO.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.cats.effect 2 | 3 | import cats.effect.IO 4 | import japgolly.webapputil.general.Effect 5 | import java.util.concurrent.TimeUnit 6 | import scala.concurrent.duration.FiniteDuration 7 | import scala.util.Try 8 | 9 | object WebappUtilEffectAsyncIO extends WebappUtilEffectAsyncIO 10 | 11 | trait WebappUtilEffectAsyncIO extends Effect.Async[IO] { 12 | 13 | override def delay[A](a: => A): IO[A] = 14 | IO(a) 15 | 16 | override def pure[A](a: A): IO[A] = 17 | IO(a) 18 | 19 | override def map[A, B](fa: IO[A])(f: A => B): IO[B] = 20 | fa.map(f) 21 | 22 | override def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = 23 | fa.flatMap(f) 24 | 25 | override def bracket[A, B](fa: IO[A])(use: A => IO[B])(release: A => IO[Unit]): IO[B] = 26 | fa.bracket(use = use)(release = release) 27 | 28 | override def async[A](f: (Try[A] => Unit) => Unit): IO[A] = { 29 | val f2: (Either[Throwable, A] => Unit) => Unit = g => f(tryA => g(tryA.toEither)) 30 | IO.async_[A](f2) 31 | } 32 | 33 | override def timeoutMs[A](d: Long)(fa: IO[A]): IO[Option[A]] = 34 | fa 35 | .attempt 36 | .timeout(FiniteDuration(d, TimeUnit.MILLISECONDS)) 37 | .attempt 38 | .flatMap { 39 | case Right(Right(a)) => IO.pure(Some(a)) 40 | case Right(Left(e)) => IO.raiseError(e) 41 | case Left(_) => IO.pure(None) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /coreCatsEffect/shared/src/main/scala/japgolly/webapputil/cats/effect/package.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.cats 2 | 3 | package object effect 4 | extends effect.Implicits 5 | -------------------------------------------------------------------------------- /coreCirce/js/src/main/scala/japgolly/webapputil/circe/JsonAjaxClientModule.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.circe 2 | 3 | import io.circe.parser.{decode => circeDecode} 4 | import japgolly.webapputil.ajax.AjaxClient 5 | 6 | trait JsonAjaxClientModule { 7 | 8 | type JsonAjaxClient = AjaxClient[JsonCodec] 9 | 10 | object JsonAjaxClient extends DefaultJsonAjaxClient 11 | 12 | trait DefaultJsonAjaxClient extends AjaxClient.Json[JsonCodec] { 13 | 14 | override protected def encode[A](p: JsonCodec[A], a: A): String = 15 | p.encoder(a).noSpacesSortKeys 16 | 17 | override protected def decode[A](p: JsonCodec[A], j: String): AjaxClient.Response[A] = 18 | circeDecode[A](j)(p.decoder) match { 19 | case Right(a) => AjaxClient.Response.success(a) 20 | case Left(e) => AjaxClient.Response(Left(JsonUtil.errorMsg(e))) 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /coreCirce/jvm/src/main/scala/japgolly/webapputil/circe/JsonAjaxClientModule.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.circe 2 | 3 | trait JsonAjaxClientModule { 4 | 5 | // Intentionally blank. 6 | 7 | // This module is only populated on the JS side. 8 | 9 | } 10 | -------------------------------------------------------------------------------- /coreCirce/shared/src/main/scala/japgolly/webapputil/circe/JsonEntrypointCodec.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.circe 2 | 3 | import io.circe._ 4 | import io.circe.parser._ 5 | import io.circe.syntax._ 6 | import japgolly.webapputil.entrypoint.EntrypointDef.Codec 7 | 8 | object JsonEntrypointCodec { 9 | 10 | def apply[A: Decoder: Encoder]: Codec[A] = 11 | new Codec[A] { 12 | 13 | override val decodeOrThrow: String => A = str => 14 | decode[A](str) match { 15 | case Right(a) => a 16 | case Left(e) => JsonUtil.errorMsg(e).throwException() 17 | } 18 | 19 | override val encode: A => String = 20 | _.asJson.noSpacesSortKeys 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /coreCirce/shared/src/main/scala/japgolly/webapputil/circe/JsonHttpClientExt.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.circe 2 | 3 | import cats.syntax.either._ 4 | import io.circe._ 5 | import io.circe.parser.parse 6 | import io.circe.syntax._ 7 | import japgolly.webapputil.http.HttpClient._ 8 | 9 | trait JsonHttpClientExt { 10 | import JsonHttpClientExt._ 11 | 12 | /** Allows `Body.json(myObject)` */ 13 | @inline final implicit def circeHttpClientBodyObjExt(a: Body.type): BodyObjExt = new BodyObjExt(a) 14 | 15 | /** Allows `Body#parseJsonBody[A]` */ 16 | @inline final implicit def circeHttpClientBodyExt(a: Body): BodyExt = new BodyExt(a) 17 | } 18 | 19 | object JsonHttpClientExt { 20 | 21 | final class BodyObjExt(private val self: Body.type) extends AnyVal { 22 | 23 | /** Allows `Body.json(myObject)` */ 24 | @inline def json = BodyObjJson 25 | } 26 | 27 | object BodyObjJson { 28 | @inline private def contentType = 29 | Some(ContentType.JsonUtf8) 30 | 31 | def apply[A: Encoder](a: A): Body.Str = 32 | Body.Str(a.asJson.noSpaces, contentType) 33 | 34 | def sortKeys[A: Encoder](a: A): Body.Str = 35 | Body.Str(a.asJson.noSpacesSortKeys, contentType) 36 | 37 | def spaces2[A: Encoder](a: A): Body.Str = 38 | Body.Str(a.asJson.spaces2, contentType) 39 | 40 | def spaces2SortKeys[A: Encoder](a: A): Body.Str = 41 | Body.Str(a.asJson.spaces2SortKeys, contentType) 42 | } 43 | 44 | // =================================================================================================================== 45 | 46 | final class BodyExt(private val self: Body) extends AnyVal { 47 | 48 | def parseJsonBody[A: Decoder]: Either[HttpJsonParseFailure, A] = 49 | self match { 50 | case body: Body.Str => 51 | if (!body.isContentTypeJsonOrEmpty) 52 | Left(HttpJsonParseFailure.NonJsonContentType(body.contentType.getOrElse(""))) 53 | else 54 | for { 55 | json <- parse(body.content).leftMap(HttpJsonParseFailure.JsonParseError.apply) 56 | a <- json.as[A].leftMap(HttpJsonParseFailure.JsonDecodeError.apply) 57 | } yield a 58 | case f: Body.Form => 59 | Left(HttpJsonParseFailure.NonJsonContentType(f.contentType)) 60 | } 61 | } 62 | } 63 | 64 | sealed trait HttpJsonParseFailure 65 | object HttpJsonParseFailure { 66 | final case class JsonParseError (failure: ParsingFailure) extends HttpJsonParseFailure 67 | final case class JsonDecodeError (failure: DecodingFailure) extends HttpJsonParseFailure 68 | final case class NonJsonContentType(contentType: String) extends HttpJsonParseFailure 69 | } 70 | -------------------------------------------------------------------------------- /coreCirce/shared/src/main/scala/japgolly/webapputil/circe/package.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil 2 | 3 | package object circe 4 | extends circe.JsonHttpClientExt 5 | with circe.JsonAjaxClientModule 6 | -------------------------------------------------------------------------------- /coreCirce/shared/src/test/scala/japgolly/webapputil/circe/HttpClientExtTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.circe 2 | 3 | import cats.Eq 4 | import japgolly.microlibs.testutil.TestUtil._ 5 | import japgolly.webapputil.http.HttpClient._ 6 | import utest._ 7 | 8 | object HttpClientExtTest extends TestSuite { 9 | 10 | private implicit def eqHttpJsonParseFailure: Eq[HttpJsonParseFailure] = 11 | Eq.fromUniversalEquals 12 | 13 | override def tests = Tests { 14 | 15 | "body" - { 16 | 17 | "json" - { 18 | val a = Body.json("he") 19 | assertEq(a, Body.Str("\"he\"", Some(ContentType.JsonUtf8))) 20 | } 21 | 22 | "parseJsonBody" - { 23 | "ok" - { 24 | val b: Body = Body.Str("\"he\"", Some(ContentType.JsonUtf8)) 25 | assertEq(b.parseJsonBody[String], Right("he")) 26 | } 27 | 28 | "wrongHeader" - { 29 | val b: Body = Body.Str("1", Some(ContentType.Binary)) 30 | assertMatch(b.parseJsonBody[String]) { case Left(_: HttpJsonParseFailure.NonJsonContentType) => } 31 | } 32 | 33 | "cantParse" - { 34 | val b: Body = Body.Str("\"he", Some(ContentType.JsonUtf8)) 35 | assertMatch(b.parseJsonBody[String]) { case Left(_: HttpJsonParseFailure.JsonParseError) => } 36 | } 37 | } 38 | 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /coreOkHttp4/src/main/scala/japgolly/webapputil/okhttp4/OkHttp4Client.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.okhttp4 2 | 3 | import japgolly.webapputil.general.{Effect, LazyVal} 4 | import japgolly.webapputil.http._ 5 | import java.io.IOException 6 | import okhttp3.{Call, Callback, FormBody, HttpUrl, MediaType, OkHttpClient => OkClient} 7 | import scala.annotation.nowarn 8 | import scala.util.{Failure, Success} 9 | 10 | object OkHttp4Client { 11 | def apply[F[_]](okhttp: OkClient)(implicit F: Effect.Async[F]): OkHttp4Client[F] = 12 | new OkHttp4Client(okhttp) 13 | } 14 | 15 | class OkHttp4Client[F[_]](okhttp: OkClient)(implicit F: Effect.Async[F]) extends HttpClient.Module[F] { 16 | 17 | @nowarn("msg=outer reference in this type test") 18 | override val HttpClient: HttpClient = { 19 | def convertRequest(req: Request): okhttp3.Request = { 20 | val url: HttpUrl = 21 | if (req.uriParams.isEmpty) 22 | HttpUrl.get(req.uri) 23 | else { 24 | val urlBuilder = HttpUrl.parse(req.uri).newBuilder() 25 | req.uriParams.asVector.foreach { case (k, v) => 26 | urlBuilder.addQueryParameter(k, v) 27 | } 28 | urlBuilder.build() 29 | } 30 | 31 | val headers: okhttp3.Headers = 32 | req 33 | .headers 34 | .asVector 35 | .foldLeft(new okhttp3.Headers.Builder) { case (b, (k, v)) => b.add(k, v) } 36 | .build() 37 | 38 | val body: okhttp3.RequestBody = 39 | req.body match { 40 | case Body.Str(content, None) => 41 | okhttp3.RequestBody.create(content.getBytes()) 42 | 43 | case Body.Str(content, Some(contentType)) => 44 | okhttp3.RequestBody.create(content, MediaType.get(contentType)) 45 | 46 | case Body.Form(params) => 47 | val formBody = new FormBody.Builder() 48 | params.asVector.foreach { case (k, v) => 49 | formBody.add(k, v) 50 | } 51 | formBody.build() 52 | } 53 | 54 | new okhttp3.Request.Builder() 55 | .url(url) 56 | .headers(headers) 57 | .method(req.method.asString, body) 58 | .build() 59 | } 60 | 61 | def convertResponse(res: okhttp3.Response): Response = { 62 | def body = ResponseBody( 63 | content = res.body().string(), 64 | contentType = Option(res.body().contentType()).map(_.toString), 65 | ) 66 | Response( 67 | status = Status(res.code()), 68 | body = LazyVal(body), 69 | headers = Headers.fromJavaMultimap(res.headers().toMultimap), 70 | ) 71 | } 72 | 73 | def exec(req: okhttp3.Request): F[okhttp3.Response] = 74 | F.async[okhttp3.Response] { callback => 75 | okhttp 76 | .newCall(req) 77 | .enqueue(new Callback { 78 | override def onFailure(call: Call, e: IOException): Unit = callback(Failure(e)) 79 | override def onResponse(call: Call, res: okhttp3.Response): Unit = callback(Success(res)) 80 | }) 81 | } 82 | 83 | req => F.map(exec(convertRequest(req)))(convertResponse) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /dbPostgres/src/main/scala/japgolly/webapputil/db/DbMigration.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.db 2 | 3 | import cats.effect.IO 4 | import javax.sql.DataSource 5 | import org.flywaydb.core.Flyway 6 | import org.flywaydb.core.api.configuration.FluentConfiguration 7 | 8 | object DbMigration { 9 | 10 | def apply(ds : DataSource, 11 | schema: Option[String] = None, 12 | flyway: FlywayConfig = FlywayConfig.default, 13 | ): DbMigration = { 14 | var cfg = flyway(Flyway.configure).dataSource(ds) 15 | schema.foreach(s => cfg = cfg.schemas(s)) 16 | new DbMigration(cfg) 17 | } 18 | 19 | type FlywayConfig = FluentConfiguration => FluentConfiguration 20 | 21 | object FlywayConfig { 22 | def default: FlywayConfig = _ 23 | .locations("db_migrations") 24 | .sqlMigrationPrefix("v") 25 | } 26 | } 27 | 28 | final class DbMigration(private val flywayCfg: FluentConfiguration) { 29 | 30 | private val flyway: Flyway = 31 | flywayCfg.load() 32 | 33 | def withFlywayConfig(f: DbMigration.FlywayConfig): DbMigration = 34 | new DbMigration(f(flywayCfg)) 35 | 36 | def migrate: IO[Unit] = 37 | IO(flyway.migrate()) 38 | 39 | /** Drops all objects (tables, views, procedures, triggers, ...) in the configured schemas. */ 40 | def drop: IO[Unit] = 41 | IO(flyway.clean()) 42 | } 43 | -------------------------------------------------------------------------------- /dbPostgres/src/main/scala/japgolly/webapputil/db/DoobieCodecs.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.db 2 | 3 | import doobie._ 4 | import doobie.postgres.implicits._ 5 | import java.time._ 6 | import org.postgresql.util.PGInterval 7 | 8 | object DoobieCodecs extends DoobieCodecs 9 | 10 | trait DoobieCodecs { 11 | 12 | protected final val UTC = ZoneId.of("UTC") 13 | 14 | implicit val doobieWriteDuration: Write[Duration] = 15 | Write[PGInterval].contramap(d => 16 | new PGInterval( 17 | 0, 0, 0, 0, 0, // years, months, days, hours, minutes 18 | d.getSeconds.toDouble + d.getNano / 1000000000.0, 19 | )) 20 | 21 | implicit val doobieMetaInstant: Meta[Instant] = 22 | Meta[OffsetDateTime].timap(_.toInstant)(OffsetDateTime.ofInstant(_, UTC)) 23 | } 24 | -------------------------------------------------------------------------------- /dbPostgres/src/main/scala/japgolly/webapputil/db/JdbcLogging.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.db 2 | 3 | import com.typesafe.scalalogging.StrictLogging 4 | 5 | object JdbcLogging extends StrictLogging with SqlTracer { 6 | 7 | override def logExecute(method: String, sql: String, batches: Int, err: Option[Throwable], startTimeNs: Long, endTimeNs: Long): Unit = { 8 | @inline def durMs = (endTimeNs - startTimeNs) / 1000000 9 | 10 | err match { 11 | case None => 12 | if (batches == 1) 13 | logger.info(s"$method($sql) completed in $durMs ms") 14 | else 15 | logger.info(s"$method($sql) with $batches batches completed in $durMs ms") 16 | 17 | case Some(t) => 18 | if (batches == 1) 19 | logger.error(s"$method($sql) failed after $durMs ms", t) 20 | else 21 | logger.error(s"$method($sql) with $batches batches failed after $durMs ms", t) 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /dbPostgres/src/main/scala/japgolly/webapputil/db/XA.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.db 2 | 3 | import cats.effect.IO 4 | import cats.~> 5 | import doobie._ 6 | 7 | class XA(val transactor: Transactor[IO]) extends (ConnectionIO ~> IO) { 8 | 9 | private[this] val trans: (ConnectionIO ~> IO) = 10 | transactor.trans 11 | 12 | override def apply[A](c: ConnectionIO[A]): IO[A] = 13 | trans(c) 14 | } 15 | -------------------------------------------------------------------------------- /examples/js/src/test/scala/japgolly/webapputil/examples/entrypoint/EntrypointExampleFrontend.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.examples.entrypoint 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.scalajs.react.vdom.html_<^._ 5 | import japgolly.webapputil.entrypoint._ 6 | import japgolly.webapputil.examples.entrypoint.EntrypointExample.InitialData 7 | import scala.scalajs.js.annotation.JSExportTopLevel 8 | 9 | @JSExportTopLevel(EntrypointExample.Name) // Instruct Scala.js to make this available in 10 | // the global JS namespace. 11 | object Frontend extends Entrypoint(EntrypointExample.defn) { 12 | 13 | // Because the line above extends Entrypoint, all we need to do now is implement a 14 | // run method that takes the decoded InitialData value. 15 | override def run(i: InitialData): Unit = { 16 | // Render a simple React component 17 | val reactApp = Component(i) 18 | reactApp.renderIntoDOM(`#root`) // `#root` is a helper to find DOM with id=root 19 | } 20 | 21 | val Component = ScalaComponent.builder[InitialData] 22 | .render_P { i => <.div(s"Hello @${i.username} and nice to meet you!") } 23 | .build 24 | } 25 | -------------------------------------------------------------------------------- /examples/js/src/test/scala/japgolly/webapputil/examples/indexeddb/IDBExampleModels.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.examples.indexeddb 2 | 3 | import java.util.UUID 4 | 5 | // Let's say these are the data types we want to store in IndexedDb... 6 | final case class PersonId(value: UUID) 7 | final case class Person(id: PersonId, name: String, age: Int) 8 | -------------------------------------------------------------------------------- /examples/js/src/test/scala/japgolly/webapputil/examples/indexeddb/IDBExampleTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.examples.indexeddb 2 | 3 | import japgolly.webapputil.binary._ 4 | import japgolly.webapputil.boopickle.test._ 5 | import japgolly.webapputil.indexeddb._ 6 | import japgolly.webapputil.test.node.TestNode.asyncTest 7 | import java.util.UUID 8 | import utest._ 9 | 10 | object IDBExampleTest extends TestSuite { 11 | 12 | private implicit def idb: IndexedDb = FakeIndexedDb() 13 | private implicit def pako: Pako = Pako.global 14 | 15 | private val encKey = BinaryData.fromStringAsUtf8("?" * 32) 16 | private val stores = TestEncryption(encKey).map(IDBExampleStores(_)) 17 | private val bob = Person(PersonId(UUID.randomUUID()), "Bob Loblaw", 100) 18 | 19 | override def tests = Tests { 20 | 21 | "saveAndReload" - asyncTest() { 22 | for { 23 | s <- stores 24 | db <- TestIndexedDb(s.people) 25 | _ <- db.put(s.people)(bob.id, bob) // save a Person instance 26 | bob2 <- db.get(s.people)(bob.id) // load a Person instance 27 | } yield { 28 | assert(bob2 == Some(bob)) 29 | } 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/jvm/src/test/scala/japgolly/webapputil/examples/ajax/AjaxExampleJvm.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.examples.ajax 2 | 3 | import io.circe.{Decoder, Json} 4 | 5 | object AjaxExampleJvm { 6 | 7 | // This takes JSON and returns JSON. 8 | // 9 | // It's left as an exercise to the reader to integrate a JSON-to-JSON endpoint into 10 | // your web server of choice. 11 | // 12 | def serveAddInts(requestJson: Json): Decoder.Result[Json] = { 13 | import AjaxExampleShared.AddInts.{logic, protocol} 14 | 15 | protocol.requestProtocol.codec.decode(requestJson).map { request => 16 | val response = logic(request) 17 | val responseJson = protocol.responseProtocol(request).codec.encode(response) 18 | responseJson 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /examples/jvm/src/test/scala/japgolly/webapputil/examples/entrypoint/EntrypointExampleBackend.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.examples.entrypoint 2 | 3 | import japgolly.webapputil.entrypoint._ 4 | 5 | // Here we demonstrate how a backend web server can generate HTML to have the client 6 | // invoke the entrypoint and start the app. 7 | object Backend { 8 | 9 | private val invoker = EntrypointInvoker(EntrypointExample.defn) 10 | 11 | // It's as simple as this: provide an input value, get HTML. 12 | // 13 | // You can expect to see something like this: 14 | // 15 | // 16 | // 17 | // which is morally equivalent to: 18 | // 19 | // 22 | // 23 | // We'll look at this in more detail in the next section. 24 | // 25 | def generateHtml(i: EntrypointExample.InitialData): Html = 26 | invoker(i).toHtmlScriptTag 27 | 28 | // The same as above, except instead of having the invocation occur immediately, 29 | // it schedules it to run on window.onload. 30 | // 31 | // This is morally equivalent to: 32 | // 33 | // 38 | // 39 | // We'll look at this in more detail in the next section, too. 40 | // 41 | def generateHtmlToRunOnWindowLoad(i: EntrypointExample.InitialData): Html = 42 | invoker(Js.Wrapper.windowOnLoad, i).toHtmlScriptTag 43 | } 44 | -------------------------------------------------------------------------------- /examples/jvm/src/test/scala/japgolly/webapputil/examples/entrypoint/EntrypointExampleBackendTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.examples.entrypoint 2 | 3 | import boopickle._ 4 | import japgolly.microlibs.testutil.TestUtil._ 5 | import japgolly.univeq.UnivEq 6 | import japgolly.webapputil.binary.BinaryData 7 | import utest._ 8 | 9 | object BackendTest extends TestSuite { 10 | import EntrypointExample.InitialData 11 | 12 | // Declare that == is fine for testing equality of InitialData instances 13 | private implicit def univEqInitialData: UnivEq[InitialData] = UnivEq.derive 14 | 15 | // Sample data 16 | private val initData = InitialData(username = "someone123") 17 | 18 | // The base64 encoding of `initData` after binary serialisation 19 | private val initData64 = "CnNvbWVvbmUxMjM=" 20 | 21 | // Helper to parse BinaryData into InitialData 22 | private def deserialiseInitialData(b: BinaryData): InitialData = 23 | UnpickleImpl(EntrypointExample.picklerInitialData).fromBytes(b.unsafeByteBuffer) 24 | 25 | override def tests = Tests { 26 | 27 | // ================================================================================= 28 | // Verify the initData64 deserialises to initData 29 | "pickler" - assertEq( 30 | deserialiseInitialData(BinaryData.fromBase64OrThrow(initData64)), 31 | initData) 32 | 33 | // ================================================================================= 34 | // Let's start with our Backend.generateHtml() method 35 | "generateHtml" - { 36 | 37 | // Verify the total HTML output 38 | val js64 = "TXlFeGFtcGxlQXBwLm0oIkNuTnZiV1Z2Ym1VeE1qTT0iKQ==" 39 | assertEq( 40 | Backend.generateHtml(initData).asString, 41 | s"""""") 42 | 43 | // Verify the JS after base64 decoding 44 | assertEq( 45 | BinaryData.fromBase64OrThrow(js64).toStringAsUtf8, 46 | s"""MyExampleApp.m("$initData64")""") 47 | } 48 | 49 | // ================================================================================= 50 | // As above, but the RunOnWindowLoad variant 51 | "generateHtmlToRunOnWindowLoad" - { 52 | 53 | // Verify the total HTML output 54 | val js64 = "d2luZG93Lm9ubG9hZD1mdW5jdGlvbigpe015RXhhbXBsZUFwcC5tKCJDbk52YldWdmJtVXhNak09Iil9Ow==" 55 | assertEq( 56 | Backend.generateHtmlToRunOnWindowLoad(initData).asString, 57 | s"""""") 58 | 59 | // Verify the JS after base64 decoding 60 | assertEq( 61 | BinaryData.fromBase64OrThrow(js64).toStringAsUtf8, 62 | s"""window.onload=function(){MyExampleApp.m("$initData64")};""") 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/shared/src/test/scala/japgolly/webapputil/examples/ajax/AjaxExampleShared.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.examples.ajax 2 | 3 | import io.circe.{Decoder, Encoder} 4 | import japgolly.webapputil.ajax.AjaxProtocol 5 | import japgolly.webapputil.circe.JsonCodec 6 | import japgolly.webapputil.general.{Protocol, Url} 7 | 8 | object AjaxExampleShared { 9 | 10 | // This is an example AJAX endpoint definition 11 | object AddInts { 12 | 13 | val url = Url.Relative("/add-ints") 14 | 15 | case class Request(m: Int, n: Int) 16 | 17 | type Response = Long 18 | 19 | // Here we define the protocol between client and server. 20 | // Specifically, it's the URL, the request and response types, plus the codecs for 21 | // serialisation & deserialisation. 22 | val protocol: AjaxProtocol.Simple[JsonCodec, Request, Response] = { 23 | 24 | implicit val decoderRequest: Decoder[Request] = 25 | Decoder.forProduct2("m", "n")(Request.apply) 26 | 27 | implicit val encoderRequest: Encoder[Request] = 28 | Encoder.forProduct2("m", "n")(a => (a.m, a.n)) 29 | 30 | val requestProtocol: Protocol.Of[JsonCodec, Request] = 31 | // this combines decoderRequest and encoderRequest above 32 | Protocol(JsonCodec.summon[Request]) 33 | 34 | val responseProtocol: Protocol.Of[JsonCodec, Response] = 35 | // uses the circe's default implicits for Long <=> Json 36 | Protocol(JsonCodec.summon[Response]) 37 | 38 | // "Simple" here means that the responseProtocol is static 39 | // (as opposed to being polymorphic / dependently-typed on the request) 40 | AjaxProtocol.Simple(url, requestProtocol, responseProtocol) 41 | } 42 | 43 | // This is here just so that it's easily available from the example JS tests. 44 | // 45 | // In a real-project you'd share as much logic with the JS tests as possible, 46 | // abstracting away things like DB access. To do things properly-properly, you'd 47 | // also use sbt modules to ensure this is only shared with the test JS and not the 48 | // main JS (so that it becomes impossible for another team-member to accidently use 49 | // the logic directly from JS and avoid the AJAX call). 50 | // 51 | def logic(req: Request): Response = 52 | req.m.toLong + req.n.toLong 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/shared/src/test/scala/japgolly/webapputil/examples/entrypoint/EntrypointExample.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.examples.entrypoint 2 | 3 | import boopickle.DefaultBasic._ 4 | import japgolly.webapputil.boopickle._ 5 | import japgolly.webapputil.entrypoint._ 6 | 7 | object EntrypointExample { 8 | 9 | // The name of our app, as it will appear in the JS global namespace when loaded. 10 | // This is final because its referenced via @JSExportTopLevel on Frontend 11 | final val Name = "MyExampleApp" 12 | 13 | // In this example, our app will request the user's username as soon as it starts up. 14 | // The server will provide this to the client. 15 | final case class InitialData(username: String) 16 | 17 | // This is our codec typeclass from InitialData to binary and back 18 | implicit val picklerInitialData: Pickler[InitialData] = 19 | implicitly[Pickler[String]].xmap(InitialData.apply)(_.username) 20 | 21 | // Finally, our entrypoint definition. 22 | // 23 | // The Pickler[InitialData] we created above is pulled in implicitly. 24 | // (Note: Binary is just one supported format, and not at all a necessity.) 25 | // 26 | val defn = EntrypointDef[InitialData](Name) 27 | } 28 | -------------------------------------------------------------------------------- /ghpages/src/docs/README.md: -------------------------------------------------------------------------------- 1 | # webapp-util 2 | 3 | 4 | ### Examples 5 | 6 | Click on the examples on the left. 7 | 8 | 9 | ### Links 10 | 11 | * Github: https://github.com/japgolly/webapp-util -------------------------------------------------------------------------------- /ghpages/src/docs/examples/ajax.md: -------------------------------------------------------------------------------- 1 | {% laika.title = AJAX %} 2 | 3 | AJAX Example 4 | ============ 5 | 6 | This is an example around making and testing AJAX calls. 7 | 8 | ## Shared Code 9 | 10 | First we create a definition of our AJAX endpoint. This is cross-compiled for both JVM and JS. 11 | 12 | @:sourceFile(AjaxExampleShared.scala) 13 | 14 | ## Backend 15 | 16 | @:sourceFile(AjaxExampleJvm.scala) 17 | 18 | ## Frontend 19 | 20 | @:sourceFile(AjaxExampleJs.scala) 21 | 22 | ## Testing the Frontend 23 | 24 | @:sourceFile(AjaxExampleJsTest.scala) 25 | -------------------------------------------------------------------------------- /ghpages/src/docs/examples/directory.conf: -------------------------------------------------------------------------------- 1 | laika.title = Examples 2 | -------------------------------------------------------------------------------- /ghpages/src/docs/examples/entrypoint.md: -------------------------------------------------------------------------------- 1 | {% laika.title = Entrypoints %} 2 | 3 | Entrypoint Example 4 | ================== 5 | 6 | An `Entrypoint` is a method on the client-side, that you must call in order to start your webapp. 7 | 8 | A typical webapp will be served like this: 9 | 10 | ```html 11 | 12 | 15 | ``` 16 | 17 | ## Why? 18 | 19 | Generating this stuff manually isn't a huge deal, but in this example we'll use the `Entrypoint` API 20 | for a few advantages: 21 | 22 | * Everything is DRY — no chance to accidentally call the wrong function 23 | * The server can provide custom data to initialise our app — just need to provide a codec, ser/deser and JS plumbing handled automatically 24 | * We avoid a typical AJAX call to initialise our app — faster and better experience for users 25 | * HTML is generated for us — things like escaping handled automatically 26 | 27 | ## Shared Definition 28 | 29 | We'll start with our entrypoint definition, which is cross-compiled for JVM and JS. 30 | 31 | @:sourceFile(EntrypointExample.scala) 32 | 33 | ## Frontend 34 | 35 | Next we'll create our Scala.js frontend. 36 | 37 | @:sourceFile(EntrypointExampleFrontend.scala) 38 | 39 | ## Backend 40 | 41 | Because we initialise our webapp with a username, the backend server needs to generate different HTML depending on who 42 | the request is for. 43 | 44 | A few important things are out of scope for this demo: 45 | 46 | * how to retrieve a username depends on your app 47 | * how to serve HTML depends on your choice of web server 48 | * how to serve the frontend JS depends on your app and your choice of web server! 49 | 50 | In our example below we simply focus on `InitialData => Html`. 51 | 52 | @:sourceFile(EntrypointExampleBackend.scala) 53 | 54 | ## Backend Test 55 | 56 | The only real value in this test is that our `Pickler[InitialData]` serialises and deserialises correctly. 57 | Everything else is just to demonstrate how exactly how the generated HTML works. 58 | 59 | @:sourceFile(EntrypointExampleBackendTest.scala) 60 | -------------------------------------------------------------------------------- /ghpages/src/docs/examples/indexeddb.md: -------------------------------------------------------------------------------- 1 | {% laika.title = IndexedDB %} 2 | 3 | IndexedDb Example 4 | ================= 5 | 6 | Below will demonstrate a few different ways of using the IndexedDB API. 7 | If you're impatient feel free to jump straight to the [Usage] section. 8 | 9 | ## Models 10 | 11 | For our demo, we start with a few model classes. 12 | 13 | @:sourceFile(IDBExampleModels.scala) 14 | 15 | ## Stores 16 | 17 | Now we'll define our IndexedDB stores (like DB tables) and demonstrate some nice 18 | features like data compression and encryption. 19 | 20 | @:sourceFile(IDBExampleStores.scala) 21 | 22 | ## Usage 23 | 24 | Here we get to actual usage. 25 | 26 | @:sourceFile(IDBExample.scala) 27 | 28 | ## Testing 29 | 30 | Here's a quick test that demonstrates we can save and load a `Person`. 31 | 32 | To test your `IndexedDb` code, you would typically: 33 | 34 | 1. Write your code in such a way that it asks for an `IndexedDb` instance as a normal function argument 35 | 2. Create a `FakeIndexedDb()` instance 36 | 3. Simply provide the `FakeIndexedDb()` to your main code 37 | 4. (Optionally) inspect the DB contents directly by using the `IndexedDb` API as normal 38 | 39 | @:sourceFile(IDBExampleTest.scala) 40 | -------------------------------------------------------------------------------- /jsBundles/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /jsBundles/Makefile: -------------------------------------------------------------------------------- 1 | WEBPACK := ./node_modules/.bin/webpack 2 | 3 | .PHONY: build 4 | 5 | build: 6 | @rm -rf dist 7 | @$(WEBPACK) 8 | -------------------------------------------------------------------------------- /jsBundles/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp-util", 3 | "version": "1", 4 | "private": true, 5 | "dependencies": { 6 | "fake-indexeddb": "3.1.7" 7 | }, 8 | "devDependencies": { 9 | "webpack": "^5.72.0", 10 | "webpack-cli": "^4.9.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jsBundles/src/fake-indexeddb.js: -------------------------------------------------------------------------------- 1 | require("fake-indexeddb/auto") 2 | -------------------------------------------------------------------------------- /jsBundles/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeModules = path.resolve(__dirname, '../node_modules'); 3 | 4 | module.exports = { 5 | 6 | entry: { 7 | 'fake-indexeddb': './src/fake-indexeddb.js', 8 | }, 9 | 10 | target: 'node', 11 | 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: '[name].js', 15 | // library: '', 16 | libraryTarget: 'global', 17 | }, 18 | 19 | resolve: { 20 | modules: [ 21 | nodeModules, 22 | 'node_modules', 23 | ], 24 | }, 25 | resolveLoader: { 26 | modules: [ 27 | nodeModules, 28 | ], 29 | }, 30 | 31 | // Using 'production' here breaks fake-indexeddb somehow. 32 | // Running the code in https://github.com/dumbmatter/fakeIndexedDB#use from Scala.JS prints: 33 | // 34 | // From index: { title: 'Quarry Memories', author: 'Fred', isbn: 123456 } 35 | // From cursor: undefined 36 | // From cursor: undefined 37 | // All done! 38 | // 39 | // instead of the expected 40 | // 41 | // From index: { title: 'Quarry Memories', author: 'Fred', isbn: 123456 } 42 | // From cursor: { title: 'Water Buffaloes', author: 'Fred', isbn: 234567 } 43 | // From cursor: { title: 'Bedrock Nights', author: 'Barney', isbn: 345678 } 44 | // All done! 45 | // 46 | mode: 'development', 47 | 48 | performance: { 49 | hints: false 50 | }, 51 | 52 | bail: true, 53 | }; 54 | -------------------------------------------------------------------------------- /project/GenJsonCodecs.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object GenJsonCodecs { 4 | 5 | def apply(srcRootDir: File): Unit = { 6 | 7 | val dir = srcRootDir / "japgolly/webapputil/circe" 8 | 9 | println() 10 | println("Generating JsonCodec boilerplate in: " + dir.getAbsolutePath) 11 | 12 | val merges = List.newBuilder[String] 13 | 14 | for (n <- 1 to 22) { 15 | val _As = (1 to n).map('A' + _ - 1).map(_.toChar) 16 | val As = _As.mkString(", ") 17 | val _as = (1 to n).map('a' + _ - 1).map(_.toChar) 18 | val as = _as.mkString(", ") 19 | val jJs = _as.map(a => s"j$a: JsonCodec[${a.toUpper}]").mkString(", ") 20 | val js = _as.map(a => s"j$a").mkString(", ") 21 | 22 | if (n > 1) { 23 | val encs = (1 to n).map { i => 24 | val a = ('a' + i - 1).toChar 25 | s" val $a = { val x = j$a.encoder(z._$i); val oo = x.asObject; if (oo.isEmpty) fail(x); oo.get }" 26 | }.mkString("\n") 27 | 28 | val decs = _as.map(a => 29 | s" $a <- j$a.decoder(cur)" 30 | ).mkString("\n") 31 | 32 | merges += 33 | s"""| def merge$n[$As](implicit $jJs): JsonCodec[($As)] = { 34 | | val enc = Encoder.instance[($As)] { z => 35 | | def fail(z: Json): Nothing = throw new IllegalStateException("Expected a JsonObject, got: " + z.noSpaces) 36 | |$encs 37 | | ${_as.mkString("Json.fromJsonObject(", " deepMerge ", ")")} 38 | | } 39 | | val dec = Decoder.instance[($As)] { cur => 40 | | for { 41 | |$decs 42 | | } yield ($as) 43 | | } 44 | | JsonCodec(enc, dec) 45 | | } 46 | |""".stripMargin 47 | } 48 | } 49 | 50 | // ================================================================================================================= 51 | 52 | def save(filename: String)(content: String): Unit = { 53 | println(s"Generating $filename ...") 54 | val c = content.trim + "\n" 55 | // println(c) 56 | IO.write(dir / filename, c) 57 | } 58 | 59 | save("JsonCodecArityBoilerplate.scala")( 60 | s"""package japgolly.webapputil.circe 61 | | 62 | |import io.circe._ 63 | | 64 | |trait JsonCodecArityBoilerplate { 65 | | 66 | |${merges.result().mkString("\n")} 67 | |} 68 | |""".stripMargin 69 | ) 70 | 71 | println() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /project/Lib.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | import com.jsuereth.sbtpgp.PgpKeys._ 4 | import sbtcrossproject.CrossProject 5 | import sbtcrossproject.CrossPlugin.autoImport._ 6 | import scalajscrossproject.ScalaJSCrossPlugin.autoImport._ 7 | import xerial.sbt.Sonatype.autoImport._ 8 | 9 | object Lib { 10 | 11 | private val cores = java.lang.Runtime.getRuntime.availableProcessors() 12 | 13 | private def readConfigVar(name: String): String = 14 | Option(System.getProperty(name)).orElse(Option(System.getenv(name))) 15 | .fold("")(_.trim.toLowerCase) 16 | 17 | val inCI = readConfigVar("CI") == "1" 18 | if (inCI) { 19 | println(s"[info] ======== CI Mode ========") 20 | println(s"[info] $cores cores available") 21 | } 22 | 23 | def scalafixEnabled = 24 | !inCI 25 | 26 | type CPE = CrossProject => CrossProject 27 | type PE = Project => Project 28 | 29 | def byScalaVersion[A](f: PartialFunction[(Long, Long), Seq[A]]): Def.Initialize[Seq[A]] = 30 | Def.setting(CrossVersion.partialVersion(scalaVersion.value).flatMap(f.lift).getOrElse(Nil)) 31 | 32 | class ConfigureBoth(val jvm: PE, val js: PE) { 33 | def jvmConfigure(f: PE) = new ConfigureBoth(f compose jvm, js) 34 | def jsConfigure(f: PE) = new ConfigureBoth(jvm, f compose js) 35 | } 36 | 37 | def ConfigureBoth(both: PE) = new ConfigureBoth(both, both) 38 | 39 | implicit def _configureBothToCPE(p: ConfigureBoth): CPE = 40 | _.jvmConfigure(p.jvm).jsConfigure(p.js) 41 | 42 | implicit class CrossProjectExt(val cp: CrossProject) extends AnyVal { 43 | def bothConfigure(fs: PE*): CrossProject = 44 | fs.foldLeft(cp)((q, f) => 45 | q.jvmConfigure(f).jsConfigure(f)) 46 | } 47 | implicit def CrossProjectExtB(b: CrossProject.Builder) = 48 | new CrossProjectExt(b) 49 | 50 | def publicationSettings(ghProject: String) = ConfigureBoth( 51 | _.settings( 52 | developers := List( 53 | Developer("japgolly", "David Barri", "japgolly@gmail.com", url("https://japgolly.github.io/japgolly/")), 54 | ), 55 | )) 56 | .jsConfigure( 57 | sourceMapsToGithub(ghProject)) 58 | 59 | def sourceMapsToGithub(ghProject: String): PE = 60 | p => p.settings( 61 | scalacOptions ++= { 62 | val isScala3 = scalaVersion.value startsWith "3" 63 | val ver = version.value 64 | if (isSnapshot.value) 65 | Nil 66 | else { 67 | val a = p.base.toURI.toString.replaceFirst("[^/]+/?$", "") 68 | val g = s"https://raw.githubusercontent.com/japgolly/$ghProject" 69 | val flag = if (isScala3) "-scalajs-mapSourceURI" else "-P:scalajs:mapSourceURI" 70 | s"$flag:$a->$g/v$ver/" :: Nil 71 | } 72 | } 73 | ) 74 | 75 | def preventPublication: PE = 76 | _.settings(publish / skip := true) 77 | } 78 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") 2 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") 3 | addSbtPlugin("org.planet42" % "laika-sbt" % "0.19.3") 4 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") 5 | addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.2") 6 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") 7 | 8 | 9 | libraryDependencies += "org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.1.0" 10 | -------------------------------------------------------------------------------- /scalafix.sbt: -------------------------------------------------------------------------------- 1 | { 2 | if (Lib.scalafixEnabled) 3 | Seq( 4 | ThisBuild / semanticdbEnabled := true, 5 | ThisBuild / scalafixScalaBinaryVersion := "2.13", 6 | ThisBuild / semanticdbVersion := "4.8.3", 7 | 8 | ThisBuild / scalacOptions ++= { 9 | if (scalaVersion.value startsWith "2") 10 | "-Yrangepos" :: "-P:semanticdb:synthetics:on" :: Nil 11 | else 12 | Nil 13 | }, 14 | 15 | ThisBuild / scalafixDependencies ++= Seq( 16 | "com.github.liancheng" %% "organize-imports" % "0.6.0" 17 | ) 18 | ) 19 | else 20 | Nil 21 | } 22 | -------------------------------------------------------------------------------- /testBoopickle/js/src/main/scala/japgolly/webapputil/boopickle/test/TestEncryption.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle.test 2 | 3 | import japgolly.scalajs.react.AsyncCallback 4 | import japgolly.webapputil.binary.{BinaryData, Encryption} 5 | import japgolly.webapputil.boopickle.EncryptionEngine 6 | import japgolly.webapputil.test.node.TestNode 7 | 8 | object TestEncryption { 9 | 10 | lazy val engine: Encryption.Engine = 11 | EncryptionEngine.from(TestNode.webCrypto) 12 | .getOrElse(sys error "Node.webCrypto not accepted as an Encryption.Engine") 13 | 14 | def apply(key: BinaryData): AsyncCallback[Encryption] = 15 | engine(key) 16 | 17 | object UnsafeTypes { 18 | implicit def binaryDataFromString(str: String): BinaryData = { 19 | val bytes = str.getBytes 20 | assert(bytes.length == str.length) 21 | BinaryData.unsafeFromArray(bytes) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /testBoopickle/js/src/main/scala/japgolly/webapputil/boopickle/test/TestIndexedDb.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle.test 2 | 3 | import japgolly.scalajs.react.{AsyncCallback, Callback} 4 | import japgolly.webapputil.indexeddb.IndexedDb._ 5 | import japgolly.webapputil.indexeddb._ 6 | import japgolly.webapputil.test.node.TestNode 7 | import org.scalajs.dom.window 8 | import scala.scalajs.js 9 | 10 | object TestIndexedDb { 11 | 12 | /** Loads a local fake-indexeddb JS bundle. 13 | * 14 | * @param path Absolute path. eg "/home/me/blah/dist/fake-indexeddb" 15 | */ 16 | def loadLocalFakeIndexedDbBundle(path: String): IndexedDb = { 17 | TestNode.require(path) 18 | js.Dynamic.global.window.indexedDB = TestNode.node.indexedDB 19 | js.Dynamic.global.window.IDBKeyRange = TestNode.node.IDBKeyRange 20 | IndexedDb(window.indexedDB.get) 21 | } 22 | 23 | // =================================================================================================================== 24 | 25 | private var prevDbIndex = 0 26 | 27 | def freshDbName(): DatabaseName = { 28 | prevDbIndex += 1 29 | DatabaseName("testdb_" + prevDbIndex) 30 | } 31 | 32 | def fresh(onOpen: OpenCallbacks)(implicit idb: IndexedDb): AsyncCallback[Database] = { 33 | val name = freshDbName() 34 | idb.open(name)(onOpen) 35 | } 36 | 37 | def apply(name: String, stores: ObjectStoreDef[_, _]*)(implicit idb: IndexedDb): AsyncCallback[Database] = 38 | apply(DatabaseName(name), stores: _*) 39 | 40 | def apply(name: DatabaseName, stores: ObjectStoreDef[_, _]*)(implicit idb: IndexedDb): AsyncCallback[Database] = 41 | idb.open(name)(createStoresOnOpen(stores: _*)) 42 | 43 | def apply(stores: ObjectStoreDef[_, _]*)(implicit idb: IndexedDb): AsyncCallback[Database] = 44 | fresh(createStoresOnOpen(stores: _*)) 45 | 46 | def unusedOpenCallbacks: OpenCallbacks = 47 | OpenCallbacks( 48 | upgradeNeeded = _ => Callback.empty, 49 | versionChange = _ => Callback.empty, 50 | closed = Callback.empty, 51 | ) 52 | 53 | def createStoresOnOpen(stores: ObjectStoreDef[_, _]*): OpenCallbacks = 54 | unusedOpenCallbacks.copy( 55 | upgradeNeeded = e => Callback.traverse(stores)(e.db.createObjectStore(_)) 56 | ) 57 | 58 | // =================================================================================================================== 59 | 60 | object UnsafeTypes { 61 | implicit def autoIndexedDbDatabaseName(s: String): DatabaseName = DatabaseName(s) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /testBoopickle/js/src/main/scala/japgolly/webapputil/boopickle/test/WebSocketTestUtil.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle.test 2 | 3 | import japgolly.webapputil.boopickle._ 4 | import japgolly.webapputil.general._ 5 | import japgolly.webapputil.websocket.WebSocketShared.{ClientToServer, ReqId, ServerToClient} 6 | 7 | object WebSocketTestUtil { 8 | 9 | final class Protocols[Req, Push](val protocolCS: Protocol.Of[SafePickler, ClientToServer[Req]], 10 | val protocolSC: Protocol.Of[SafePickler, ServerToClient[SafePickler, Push]]) 11 | 12 | object Protocols { 13 | 14 | def apply(p: Protocol.WebSocket.ClientReqServerPush[SafePickler]): Protocols[p.Req, p.Push] = { 15 | implicit def picklerReq: SafePickler[p.Req] = p.req.codec 16 | implicit def picklerPush: SafePickler[p.Push] = p.push.codec 17 | 18 | new Protocols[p.Req, p.Push]( 19 | BoopickleWebSocketClient.mkProtocolClientServer, 20 | BoopickleWebSocketClient.mkProtocolServerClient(responseUnpickler)) 21 | } 22 | 23 | def responseUnpickler[Codec[_]]: ReqId => Option[Protocol[Codec]] = 24 | _ => ErrorMsg("Server doesn't unpickle responses.").throwException() 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /testBoopickle/js/src/test/scala/japgolly/webapputil/boopickle/test/EncryptionTest.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle.test 2 | 3 | import japgolly.microlibs.testutil.TestUtil._ 4 | import japgolly.webapputil.binary._ 5 | import japgolly.webapputil.boopickle.test.TestEncryption.UnsafeTypes._ 6 | import japgolly.webapputil.test.node.TestNode.asyncTest 7 | import utest._ 8 | 9 | object EncryptionTest extends TestSuite { 10 | 11 | override def tests = Tests { 12 | 13 | "main" - asyncTest() { 14 | 15 | val key1 = "x" * 32 16 | val key2 = "y" * 32 17 | val src1 = "hello there!" 18 | val src2 = "awesome" 19 | 20 | for { 21 | e1 <- TestEncryption(key1) 22 | enc1 <- e1.encrypt(src1) 23 | enc1b <- e1.encrypt(src1) 24 | dec1 <- e1.decrypt(enc1) 25 | dec1b <- e1.decrypt(enc1b) 26 | 27 | e2 <- TestEncryption(key2) 28 | enc2 <- e2.encrypt(src2) 29 | e2b <- TestEncryption(key2) 30 | dec2 <- e2b.decrypt(enc2) 31 | 32 | bad12 <- e1.decrypt(enc2).attempt 33 | bad21 <- e2.decrypt(enc1).attempt 34 | 35 | } yield { 36 | val srcBin1: BinaryData = src1 37 | val srcBin2: BinaryData = src2 38 | 39 | // round trip 40 | assertEq(dec1, srcBin1) 41 | assertEq(dec1b, srcBin1) 42 | assertEq(dec2, srcBin2) 43 | 44 | // non-determinism 45 | assertNotEq(enc1, enc1b) 46 | 47 | // key protection 48 | assert(bad12.isLeft) 49 | assert(bad21.isLeft) 50 | 51 | s""" 52 | |src1 = ${srcBin1.describe()} 53 | |enc1 = ${enc1.describe()} 54 | |enc1b = ${enc1b.describe()} 55 | |src2 = ${srcBin2.describe()} 56 | |enc2 = ${enc2.describe()} 57 | |bad = $bad12 58 | |""".stripMargin.trim 59 | } 60 | } 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /testBoopickle/js/src/test/scala/japgolly/webapputil/boopickle/test/FakeIndexedDb.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.boopickle.test 2 | 3 | import japgolly.webapputil.indexeddb._ 4 | import japgolly.webapputil.test.node.TestNode 5 | 6 | object FakeIndexedDb { 7 | 8 | private lazy val instance: IndexedDb = { 9 | val sbtRootDir = TestNode.envVarNeed("SBT_ROOT") 10 | val distDir = sbtRootDir + "/jsBundles/dist" 11 | TestIndexedDb.loadLocalFakeIndexedDbBundle(s"$distDir/fake-indexeddb") 12 | } 13 | 14 | implicit def apply(): IndexedDb = 15 | instance 16 | } 17 | -------------------------------------------------------------------------------- /testCatsEffect/jvm/src/main/scala/japgolly/webapputil/cats/effect/test/package.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.cats.effect 2 | 3 | import cats.effect.IO 4 | import cats.effect.unsafe.IORuntime 5 | import cats.effect.unsafe.implicits.global 6 | import japgolly.webapputil.test.TestHttpClient 7 | 8 | package object test { 9 | 10 | type TestHttpClientIO = TestHttpClientIO.Client 11 | 12 | object TestHttpClientIO extends TestHttpClient.Module[IO, IO] { 13 | 14 | def withIORuntime(autoRespondInitially: Boolean)(implicit r: IORuntime): Client = { 15 | val e = webappUtilEffectIO(r) 16 | new TestHttpClient(autoRespondInitially)(e, e) 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /testCirce/js/src/main/scala/japgolly/webapputil/circe/test/package.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.circe 2 | 3 | import japgolly.webapputil.circe.JsonCodec 4 | import japgolly.webapputil.test.TestAjaxClient 5 | 6 | package object test { 7 | 8 | object TestJsonAjaxClient extends TestAjaxClient.Module { 9 | override type Codec[A] = JsonCodec[A] 10 | } 11 | 12 | type TestJsonAjaxClient = TestAjaxClient[JsonCodec] 13 | } 14 | -------------------------------------------------------------------------------- /testCore/js/src/main/scala/japgolly/webapputil/test/TestState.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.test 2 | 3 | trait TestState extends teststate.Exports { 4 | type Id[A] = A 5 | } 6 | 7 | object TestState extends TestState 8 | -------------------------------------------------------------------------------- /testCore/js/src/main/scala/japgolly/webapputil/test/TestTimersJs.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.test 2 | 3 | import japgolly.webapputil.general.TimersJs 4 | import java.time.Instant 5 | import scala.annotation.tailrec 6 | import scala.scalajs.js.timers.{SetIntervalHandle, SetTimeoutHandle} 7 | import scala.util.Try 8 | 9 | final class TestTimersJs extends TimersJs { 10 | import TestTimersJs._ 11 | 12 | private var queue = List.empty[Submission] 13 | private var prevId = 0 14 | 15 | private def submit(mkId: Int => Id, interval: Double, body: => Unit): Id = { 16 | prevId += 1 17 | val id = mkId(prevId) 18 | val s = Submission(id, Instant.now(), interval, () => body) 19 | queue ::= s 20 | id 21 | } 22 | 23 | private def clear(id: Id): Unit = 24 | queue = queue.filter(_.id != id) 25 | 26 | override def setTimeout(interval: Double)(body: => Unit): SetTimeoutHandle = 27 | submit(TimeoutId.apply, interval, body).asInstanceOf[SetTimeoutHandle] 28 | 29 | override def clearTimeout(handle: SetTimeoutHandle): Unit = 30 | (handle: Any) match { 31 | case id: TimeoutId => clear(id) 32 | case _ => 33 | } 34 | 35 | override def setInterval(interval: Double)(body: => Unit): SetIntervalHandle = 36 | submit(IntervalId.apply, interval, body).asInstanceOf[SetIntervalHandle] 37 | 38 | override def clearInterval(handle: SetIntervalHandle): Unit = 39 | (handle: Any) match { 40 | case id: IntervalId => clear(id) 41 | case _ => 42 | } 43 | 44 | private def runSubmission(task: Submission, resubmitIntervals: Boolean): Try[Unit] = { 45 | queue = queue.filter(_ ne task) 46 | if (resubmitIntervals) 47 | task.id match { 48 | case _: TimeoutId => () 49 | case _: IntervalId => queue ::= task.copy(submittedAt = Instant.now()) 50 | } 51 | Try(task.body()) 52 | } 53 | 54 | private def runNext(resubmitIntervals: Boolean): Option[Try[Unit]] = 55 | Option.unless(queue.isEmpty) { 56 | val task = queue.minBy(_.runAt) 57 | runSubmission(task, resubmitIntervals) 58 | } 59 | 60 | def runNext(): Option[Try[Unit]] = 61 | runNext(resubmitIntervals = true) 62 | 63 | def runAll(): List[Try[Unit]] = { 64 | val q = queue 65 | q.map(runSubmission(_, resubmitIntervals = true)) 66 | } 67 | 68 | def drain(): List[Try[Unit]] = { 69 | @tailrec def go(result: List[Try[Unit]]): List[Try[Unit]] = 70 | runNext(resubmitIntervals = false) match { 71 | case Some(r) => go(result :+ r) 72 | case None => result 73 | } 74 | go(runAll()) 75 | } 76 | 77 | def isEmpty = queue.isEmpty 78 | @inline def nonEmpty = !isEmpty 79 | } 80 | 81 | object TestTimersJs { 82 | 83 | def apply(): TestTimersJs = 84 | new TestTimersJs 85 | 86 | private final case class Submission(id: Id, submittedAt: Instant, intervalMs: Double, body: () => Unit) { 87 | val runAt: Instant = 88 | submittedAt.plusMillis(intervalMs.toLong) 89 | } 90 | 91 | private sealed trait Id 92 | private final case class TimeoutId(value: Int) extends Id 93 | private final case class IntervalId(value: Int) extends Id 94 | } 95 | -------------------------------------------------------------------------------- /testCore/js/src/main/scala/japgolly/webapputil/test/TestWebWorker.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.test 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.webapputil.webworker.AbstractWebWorker.TransferList 5 | import japgolly.webapputil.webworker._ 6 | import scala.scalajs.js 7 | 8 | object TestWebWorker { 9 | 10 | final class Client(server: () => Server) extends AbstractWebWorker.Client { 11 | 12 | private var onMessage: js.Any => Callback = 13 | _ => Callback.empty 14 | 15 | override def onError(f: OnError): Callback = 16 | Callback.empty 17 | 18 | override def listen(f: js.Any => Callback): Callback = 19 | Callback { 20 | onMessage = f 21 | } 22 | 23 | override def send(msg: js.Any, transferList: TransferList): Callback = 24 | Callback(server().recvFromClient(this, msg)) 25 | 26 | def connect(s: Server): Unit = 27 | s.connect(this) 28 | 29 | def recvFromServer(msg: js.Any): Unit = { 30 | onMessage(msg).runNow() 31 | } 32 | } 33 | 34 | // =================================================================================================================== 35 | 36 | final class Server extends AbstractWebWorker.Server { 37 | override type Client = TestWebWorker.Client 38 | 39 | private var ports: List[Port] = 40 | Nil 41 | 42 | private var onConnect: Client => CallbackTo[js.Any => Callback] = 43 | _ => CallbackTo.pure(_ => Callback.empty) 44 | 45 | override def onError(f: OnError): Callback = 46 | Callback.empty 47 | 48 | override def listen(f: Client => CallbackTo[js.Any => Callback]): Callback = 49 | Callback { 50 | onConnect = f 51 | } 52 | 53 | override def send(to: IterableOnce[Client], msg: js.Any, transferList: TransferList): Callback = 54 | Callback { 55 | for (c <- to.iterator) 56 | c.recvFromServer(msg) 57 | } 58 | 59 | def newClient(connect: Boolean = true): Client = { 60 | val c = new Client(() => this) 61 | if (connect) 62 | this.connect(c) 63 | c 64 | } 65 | 66 | def connect(c: Client): Unit = { 67 | val port = new Port(c) 68 | ports ::= port 69 | port.onMessage = onConnect(c).runNow() 70 | } 71 | 72 | def recvFromClient(client: Client, msg: js.Any): Unit = { 73 | val port = ports.find(_.client eq client).getOrElse(sys error "Client not connected") 74 | port.onMessage(msg).runNow() 75 | } 76 | } 77 | 78 | final class Port(val client: Client) { 79 | var onMessage: js.Any => Callback = 80 | _ => Callback.empty 81 | } 82 | 83 | // =================================================================================================================== 84 | 85 | def pair(): (Client, Server) = { 86 | val s = new Server 87 | val c = s.newClient() 88 | (c, s) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /testCore/js/src/main/scala/japgolly/webapputil/test/TestWindowConfirm.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.test 2 | 3 | import japgolly.scalajs.react.CallbackTo 4 | import japgolly.webapputil.browser.WindowConfirm 5 | 6 | final case class TestWindowConfirm() extends WindowConfirm { 7 | var nextResponse = true 8 | 9 | private var _calls = 0 10 | def calls() = _calls 11 | 12 | override def apply(msg: String): CallbackTo[Boolean] = 13 | CallbackTo { 14 | _calls += 1 15 | nextResponse 16 | } 17 | } 18 | 19 | object TestWindowConfirm { 20 | import TestState._ 21 | 22 | class Obs(t: TestWindowConfirm) { 23 | val calls = t.calls() 24 | } 25 | 26 | final class TestDsl[R, O, S](val * : Dsl[Id, R, O, S, String]) 27 | (getRef: R => TestWindowConfirm, 28 | getObs: O => Obs) { 29 | private implicit def autoRef(r: R): TestWindowConfirm = getRef(r) 30 | private implicit def autoObs(o: O): Obs = getObs(o) 31 | 32 | val calls = *.focus("window.confirm calls").value(_.obs.calls) 33 | 34 | def setNextResponse(r: Boolean): *.Actions = 35 | *.action("Set next WindowConfirm response to " + r)(_.ref.nextResponse = r) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /testCore/js/src/main/scala/japgolly/webapputil/test/TestWindowLocation.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.test 2 | 3 | import japgolly.scalajs.react._ 4 | import japgolly.webapputil.browser.WindowLocation 5 | import japgolly.webapputil.general.Url 6 | 7 | final class TestWindowLocation(initial: Url.Absolute) extends WindowLocation { 8 | 9 | var href = initial 10 | 11 | override def setHref(url: Url.Absolute) = Callback { 12 | href = url 13 | } 14 | 15 | override def setHrefRelative(url: Url.Relative) = Callback { 16 | href = href / url 17 | } 18 | } 19 | 20 | object TestWindowLocation { 21 | def apply(): TestWindowLocation = 22 | new TestWindowLocation(Url.Absolute("http://localhost")) 23 | } 24 | -------------------------------------------------------------------------------- /testCore/js/src/main/scala/japgolly/webapputil/test/TestWindowPrompt.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.test 2 | 3 | import japgolly.scalajs.react.CallbackTo 4 | import japgolly.webapputil.browser.WindowPrompt 5 | 6 | final case class TestWindowPrompt() extends WindowPrompt { 7 | 8 | var response: Option[Option[String]] = None 9 | var nextResponse: Option[Option[String]] = None 10 | 11 | private var _calls = 0 12 | def calls() = _calls 13 | 14 | def setNextResponse(o: Option[String]): Unit = 15 | nextResponse = Some(o) 16 | 17 | def setNextResponse(s: String): Unit = 18 | setNextResponse(Some(s)) 19 | 20 | override def apply(message: String, default: String) = 21 | apply(message) 22 | 23 | override def apply(message: String) = 24 | CallbackTo { 25 | _calls += 1 26 | (nextResponse, response) match { 27 | case (Some(r), _) => nextResponse = None; r 28 | case (None, Some(r)) => r 29 | case (None, None) => sys.error("No test response available in TestWindowPrompt") 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /testCore/shared/src/main/scala/japgolly/webapputil/test/BinaryTestUtil.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.test 2 | 3 | import japgolly.microlibs.testutil.TestUtil._ 4 | import japgolly.webapputil.binary.BinaryData 5 | import sourcecode.Line 6 | 7 | object BinaryTestUtil extends BinaryTestUtil 8 | 9 | trait BinaryTestUtil { 10 | 11 | def assertBinaryEq(actual: BinaryData, expect: BinaryData)(implicit l: Line): Unit = 12 | binaryDiff(actual, expect).foreach(fail(_)) 13 | 14 | def binaryDiff(actual : BinaryData, 15 | expect : BinaryData, 16 | descActual: String = "Actual", 17 | descExpect: String = "Expect", 18 | limit : Int = 100 19 | ): Option[String] = 20 | Option.when(actual !=* expect) { 21 | 22 | var failures = List.empty[String] 23 | 24 | if (actual.length != expect.length) 25 | failures ::= s"Actual length (${actual.length}) != expect ${expect.length}" 26 | 27 | var b1 = actual 28 | var b2 = expect 29 | 30 | var pre = "" 31 | var post = "" 32 | 33 | def tooBig() = b1.length > limit || b2.length > limit 34 | 35 | if (tooBig()) { 36 | while (tooBig() && b1.unsafeArray(0) == b2.unsafeArray(0)) { 37 | b1 = b1.drop(1) 38 | b2 = b2.drop(1) 39 | pre = "…" 40 | } 41 | 42 | if (tooBig()) { 43 | b1 = b1.take(limit) 44 | b2 = b2.take(limit) 45 | post = "…" 46 | } 47 | } 48 | 49 | val (s1, s2) = { 50 | var r1 = Console.BLACK_B 51 | var r2 = Console.BLACK_B 52 | var h1 = b1.hex 53 | var h2 = b2.hex 54 | while (h1.nonEmpty || h2.nonEmpty) { 55 | val b1 = h1.take(2) 56 | val b2 = h2.take(2) 57 | if (b1 ==* b2) { 58 | r1 += b1 59 | r2 += b2 60 | } else { 61 | r1 += Console.YELLOW_B + b1 + Console.BLACK_B 62 | r2 += Console.YELLOW_B + b2 + Console.BLACK_B 63 | } 64 | h1 = h1.drop(2) 65 | h2 = h2.drop(2) 66 | } 67 | (r1, r2) 68 | } 69 | failures :+= 70 | s""" 71 | |$descActual: $pre$s1$post 72 | |$descExpect: $pre$s2$post 73 | |""".stripMargin 74 | 75 | failures.mkString("\n") 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /testDbPostgres/src/main/scala/japgolly/webapputil/db/test/DbTable.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.db.test 2 | 3 | import cats.instances.list._ 4 | import cats.syntax.traverse._ 5 | import doobie._ 6 | import doobie.implicits._ 7 | import japgolly.microlibs.stdlib_ext.MutableArray 8 | import japgolly.microlibs.utils.AsciiTable 9 | import japgolly.univeq._ 10 | import japgolly.webapputil.db.DoobieHelpers._ 11 | 12 | final case class DbTable(name: String) { 13 | override def toString = name 14 | 15 | val count: ConnectionIO[Int] = 16 | Query0[Int]("select count(*) from " + name).unique 17 | 18 | def truncate: ConnectionIO[Unit] = 19 | Update0(s"truncate table $name cascade", None).execute 20 | } 21 | 22 | object DbTable { 23 | implicit def univEq: UnivEq[DbTable] = UnivEq.derive 24 | 25 | def all(schema: Option[String]): ConnectionIO[Set[DbTable]] = { 26 | val s = schema.getOrElse("public") 27 | sql"select tablename from pg_tables where schemaname = $s" 28 | .query[String] 29 | .to[Iterator] 30 | .map( 31 | _.filterNot(_.contains("flyway")) 32 | .map(apply) 33 | .toSet) 34 | } 35 | 36 | final case class Counts(asMap: Map[DbTable, Int]) { 37 | 38 | def apply(t: DbTable): Int = 39 | asMap.getOrElse(t, 0) 40 | 41 | def isEmpty: Boolean = 42 | asMap.values.forall(_ ==* 0) 43 | 44 | def nonEmpty: Boolean = 45 | !isEmpty 46 | 47 | def +(other: Counts): Counts = 48 | Counts(asMap.map { case (t, n) => (t, n - other(t)) }) 49 | 50 | def -(before: Counts): Counts = 51 | Counts(asMap.map { case (t, n) => (t, n - before(t)) }) 52 | 53 | def toTable: String = 54 | AsciiTable( 55 | List("TABLE", "ROWS") :: 56 | MutableArray(asMap.iterator.map(r => r._1.name :: r._2.toString :: Nil)).sortBy(_.head).iterator().toList) 57 | } 58 | 59 | def countAll(tables: IterableOnce[DbTable]): ConnectionIO[Counts] = 60 | Query0[(String, Int)]( 61 | tables 62 | .iterator 63 | .map(t => s"select '${t.name}',count(1) from ${t.name}") 64 | .mkString(" union "), 65 | ).to[Iterator].map { it => 66 | val m = it.map { case (t, n) => apply(t) -> n }.toMap 67 | Counts(m) 68 | } 69 | 70 | def truncateAll(tables: IterableOnce[DbTable]): ConnectionIO[Unit] = 71 | tables.iterator.map(_.truncate).toList.sequence.void 72 | } 73 | -------------------------------------------------------------------------------- /testDbPostgres/src/main/scala/japgolly/webapputil/db/test/ImperativeXA.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.db.test 2 | 3 | import cats.effect.Resource 4 | import doobie._ 5 | import japgolly.webapputil.db._ 6 | import japgolly.webapputil.db.test.TestDbHelpers._ 7 | 8 | final class ImperativeXA(val xa: XA, realDb: Db, lazyTables: () => Set[DbTable]) extends TestXA(xa, lazyTables) { 9 | 10 | lazy val db: Db = 11 | Db( 12 | realDb.config, 13 | realDb.dataSource, // hmmm... 14 | Resource.pure(xa), 15 | realDb.migration) 16 | 17 | override def ![A](c: ConnectionIO[A]): A = 18 | xa(c).unsafeRun() 19 | } 20 | -------------------------------------------------------------------------------- /testDbPostgres/src/main/scala/japgolly/webapputil/db/test/TestDbConfig.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.db.test 2 | 3 | import cats.Eval 4 | import japgolly.clearconfig._ 5 | import japgolly.webapputil.db.DbConfig 6 | 7 | /** Extend this to create your own: 8 | * 9 | * {{{ 10 | * object TestConfig extends japgolly.webapputil.db.test.TestDbConfig { 11 | * override protected def propsFilename = 12 | * "blah.properties" 13 | * } 14 | * }}} 15 | */ 16 | trait TestDbConfig { 17 | 18 | protected def propsPrefixDb = 19 | "db." 20 | 21 | protected def propsFilename = 22 | "test.properties" 23 | 24 | protected def propsFilenameOptional = 25 | true 26 | 27 | protected def configSources: ConfigSources[Eval] = 28 | // Highest pri 29 | ConfigSource.environment[Eval] > 30 | ConfigSource.propFileOnClasspath[Eval](propsFilename, optional = propsFilenameOptional) > 31 | ConfigSource.system[Eval] 32 | // Lowest pri 33 | 34 | protected def loadDbConfig[A](defn: ConfigDef[A]): A = 35 | defn.run(configSources).map(_.getOrDie()).value 36 | 37 | protected def dbAppName: Option[String] = 38 | None 39 | 40 | protected def dbConfig: ConfigDef[DbConfig] = 41 | DbConfig.config(defaultAppName = dbAppName).withPrefix(propsPrefixDb) 42 | 43 | protected def loadDbConfigOrThrow(): DbConfig = 44 | loadDbConfig(dbConfig) 45 | 46 | lazy val db: DbConfig = 47 | loadDbConfigOrThrow() 48 | } 49 | -------------------------------------------------------------------------------- /testDbPostgres/src/main/scala/japgolly/webapputil/db/test/TestXA.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.db.test 2 | 3 | import doobie._ 4 | import japgolly.webapputil.db.XA 5 | import japgolly.webapputil.db.test.TestDbHelpers._ 6 | 7 | class TestXA(xa: XA, lazyTables: () => Set[DbTable]) extends XA(xa.transactor) with TestDbHelpers { 8 | 9 | override def tables: Set[DbTable] = 10 | lazyTables() 11 | 12 | override def ![A](query: ConnectionIO[A]): A = 13 | this(query).unsafeRunSync() 14 | } 15 | -------------------------------------------------------------------------------- /testNode/src/main/scala/japgolly/webapputil/test/node/TestNode.scala: -------------------------------------------------------------------------------- 1 | package japgolly.webapputil.test.node 2 | 3 | import japgolly.microlibs.testutil.TestUtil._ 4 | import japgolly.scalajs.react.AsyncCallback 5 | import org.scalajs.dom.Crypto 6 | import scala.concurrent.Future 7 | import scala.scalajs.js 8 | 9 | /** Node JS access provided by `project/AdvancedNodeJSEnv.scala`. */ 10 | trait TestNode { 11 | 12 | @inline def window = js.Dynamic.global.window 13 | @inline def node = window.node 14 | 15 | def require(path: String): js.Dynamic = 16 | node.require(path).asInstanceOf[js.Dynamic] 17 | 18 | def envVarGet(name: String): js.UndefOr[String] = 19 | node.process.env.selectDynamic(name).asInstanceOf[js.UndefOr[String]] 20 | 21 | def envVarNeed(name: String): String = 22 | envVarGet(name).getOrElse(throw new RuntimeException("Missing env var: " + name)) 23 | 24 | val inCI = envVarGet("CI").contains("1") 25 | var asyncTestTimeout = if (inCI) 60000 else 3000 26 | 27 | def asyncTest[A](timeoutMs: Int = asyncTestTimeout)(ac: AsyncCallback[A]): Future[A] = 28 | ac.timeoutMs(timeoutMs).map { 29 | case Some(a) => a 30 | case None => fail(s"Async test timed out after ${timeoutMs / 1000} sec.") 31 | }.unsafeToFuture() 32 | 33 | lazy val webCrypto: Crypto = { 34 | // https://github.com/nodejs/node/pull/35093 35 | require("crypto").webcrypto.asInstanceOf[Crypto] 36 | } 37 | } 38 | 39 | object TestNode extends TestNode 40 | --------------------------------------------------------------------------------