├── images └── main.png ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── Dockerfile ├── src ├── setupTests.js ├── App.js ├── App.test.js ├── pages │ └── MessageListPage.js ├── index.css ├── protos │ ├── chat.proto │ ├── chat_pb.js │ └── chat_grpc_web_pb.js ├── components │ └── chat │ │ └── MessageList.js ├── index.js ├── App.css ├── containers │ └── MessageListContainer.js ├── logo.svg └── serviceWorker.js ├── package.json ├── server.js ├── envoy.yaml ├── .gitignore └── README.md /images/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parkgeonhu/react-gRPC-chat/HEAD/images/main.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parkgeonhu/react-gRPC-chat/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parkgeonhu/react-gRPC-chat/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parkgeonhu/react-gRPC-chat/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM envoyproxy/envoy:v1.12.2 2 | 3 | COPY ./envoy.yaml /etc/envoy/envoy.yaml 4 | 5 | CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml -l trace --log-path /tmp/envoy_info.log -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | import PostListPage from './pages/MessageListPage'; 5 | 6 | function App() { 7 | return ( 8 | <> 9 | 10 | 11 | ); 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/pages/MessageListPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MessageList from '../components/chat/MessageList'; 3 | import MessageListContainer from '../containers/MessageListContainer'; 4 | 5 | const PostListPage = () => { 6 | return ( 7 | <> 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default PostListPage; 14 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/protos/chat.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; //Specify proto3 version. 2 | 3 | package example; //Optional: unique package name. 4 | 5 | service Chat { //Service class to be used by the clients 6 | rpc join(Message) returns (stream Message){} 7 | rpc send(Message) returns (Message){} 8 | } 9 | 10 | message Message { //Information that will be passed between client and service 11 | string user = 1; 12 | string text = 2; 13 | } -------------------------------------------------------------------------------- /src/components/chat/MessageList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const MessageList = ({ chat }) => { 4 | 5 | return ( 6 |
7 | {chat.map(({ name, msg }, idx) => ( 8 |
9 | {name}: 10 | 11 | {msg} 12 |
13 | ))} 14 |
15 | ); 16 | }; 17 | 18 | export default MessageList; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-grpc-chat", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@grpc/proto-loader": "^0.5.5", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "google-protobuf": "^3.12.4", 11 | "grpc": "^1.24.3", 12 | "grpc-web": "^1.2.0", 13 | "protobufjs": "^6.10.1", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1", 16 | "react-scripts": "3.4.1" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | let grpc = require("grpc"); 2 | var protoLoader = require("@grpc/proto-loader"); 3 | 4 | const server = new grpc.Server(); 5 | const SERVER_ADDRESS = "localhost:8080"; 6 | 7 | // Load protobuf 8 | let proto = grpc.loadPackageDefinition( 9 | protoLoader.loadSync("src/protos/chat.proto", { 10 | keepCase: true, 11 | longs: String, 12 | enums: String, 13 | defaults: true, 14 | oneofs: true 15 | }) 16 | ); 17 | 18 | let users = []; 19 | 20 | // Receive message from client joining 21 | function join(call, callback) { 22 | console.log(call) 23 | users.push(call); 24 | notifyChat({ user: "Server", text: "new user joined ..." }); 25 | } 26 | 27 | // Receive message from client 28 | function send(call, callback) { 29 | console.log(call.request); 30 | 31 | notifyChat(call.request); 32 | console.log(callback) 33 | return callback(null,{text:call.request.text}) 34 | } 35 | 36 | // Send message to all connected clients 37 | function notifyChat(message) { 38 | users.forEach(user => { 39 | user.write(message); 40 | }); 41 | } 42 | 43 | // Define server with the methods and start it 44 | server.addService(proto.example.Chat.service, { join: join, send: send }); 45 | 46 | server.bind(SERVER_ADDRESS, grpc.ServerCredentials.createInsecure()); 47 | 48 | console.log("Start Server!"); 49 | 50 | server.start(); -------------------------------------------------------------------------------- /envoy.yaml: -------------------------------------------------------------------------------- 1 | admin: 2 | access_log_path: /tmp/admin_access.log 3 | address: 4 | socket_address: { address: 0.0.0.0, port_value: 9901 } 5 | 6 | static_resources: 7 | listeners: 8 | - name: listener_0 9 | address: 10 | socket_address: { address: 0.0.0.0, port_value: 9090 } 11 | filter_chains: 12 | - filters: 13 | - name: envoy.http_connection_manager 14 | config: 15 | codec_type: auto 16 | stat_prefix: ingress_http 17 | route_config: 18 | name: local_route 19 | virtual_hosts: 20 | - name: local_service 21 | domains: ["*"] 22 | routes: 23 | - match: { prefix: "/" } 24 | route: 25 | cluster: ping_pong_service 26 | max_grpc_timeout: 0s 27 | cors: 28 | allow_origin: 29 | - "*" 30 | allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout 31 | expose_headers: grpc-status,grpc-message 32 | http_filters: 33 | - name: envoy.grpc_web 34 | - name: envoy.cors 35 | - name: envoy.router 36 | clusters: 37 | - name: ping_pong_service 38 | connect_timeout: 0.25s 39 | type: logical_dns 40 | http2_protocol_options: {} 41 | lb_policy: round_robin 42 | hosts: [{ socket_address: { address: host.docker.internal, port_value: 8080 }}] -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | Footer 28 | © 2022 GitHub, Inc. 29 | Footer navigation 30 | Terms 31 | Privacy 32 | Security 33 | 34 | # Logs 35 | logs 36 | *.log 37 | npm-debug.log* 38 | yarn-debug.log* 39 | yarn-error.log* 40 | 41 | # Runtime data 42 | pids 43 | *.pid 44 | *.seed 45 | *.pid.lock 46 | 47 | # Directory for instrumented libs generated by jscoverage/JSCover 48 | lib-cov 49 | 50 | # Coverage directory used by tools like istanbul 51 | coverage 52 | 53 | # nyc test coverage 54 | .nyc_output 55 | 56 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 57 | .grunt 58 | 59 | # Bower dependency directory (https://bower.io/) 60 | bower_components 61 | 62 | # node-waf configuration 63 | .lock-wscript 64 | 65 | # Compiled binary addons (http://nodejs.org/api/addons.html) 66 | build/Release 67 | 68 | # Dependency directories 69 | node_modules/ 70 | jspm_packages/ 71 | 72 | # Distribution directories 73 | dist/ 74 | 75 | # Typescript v1 declaration files 76 | typings/ 77 | 78 | # Optional npm cache directory 79 | .npm 80 | 81 | # Optional eslint cache 82 | .eslintcache 83 | 84 | # Optional REPL history 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | *.tgz 89 | 90 | # Yarn Integrity file 91 | .yarn-integrity 92 | 93 | # dotenv environment variables file 94 | .env 95 | -------------------------------------------------------------------------------- /src/containers/MessageListContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import MessageList from '../components/chat/MessageList'; 3 | 4 | const MessageListContainer = () => { 5 | 6 | const [name, setName] = useState(""); 7 | const [msg, setMsg] = useState(""); 8 | const [chat, setChat] = useState([]); 9 | 10 | const { ChatClient } = require('../protos/chat_grpc_web_pb'); 11 | const { Message } = require('../protos/chat_pb.js'); 12 | 13 | const client= new ChatClient('http://localhost:9090', null, null); 14 | 15 | useEffect(() => { 16 | let streamRequest = new Message(); 17 | streamRequest.setUser("user"); 18 | 19 | var stream = client.join( 20 | streamRequest, 21 | null 22 | ); 23 | 24 | stream.on('data', function(response) { 25 | console.log(response); 26 | setChat(c=>[...c, { name : response.array[0], msg : response.array[1] }]); 27 | }); 28 | 29 | return () => { 30 | }; 31 | 32 | },[]); 33 | 34 | const onNameChange = e => { 35 | setName(e.target.value); 36 | } 37 | 38 | const onMsgChange = e => { 39 | setMsg(e.target.value); 40 | } 41 | 42 | const onMessageSubmit = () => { 43 | setMsg(""); 44 | const request = new Message(); 45 | request.setText(msg); 46 | request.setUser(name); 47 | client.send(request, {}, (err, response) => { 48 | if (response == null) { 49 | console.log(err) 50 | }else { 51 | console.log(response) 52 | } 53 | }); 54 | } 55 | 56 | return ( 57 |
58 | 59 | 60 | Nickname : 61 | 66 |
67 | Message : 68 | 73 | 74 | 75 |
76 | ); 77 | }; 78 | 79 | export default MessageListContainer; 80 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat application using gRPC, React 2 | 3 | ![main](images/main.png) 4 | 5 | ## How to use? 6 | 7 | ### Docker 8 | 9 | ```undefined 10 | docker build -t parkgeonhu/envoy . 11 | ``` 12 | 13 | ```undefined 14 | docker run -d -p 9090:9090 parkgeonhu/envoy 15 | ``` 16 | 17 | ### gRPC sever 18 | 19 | ``` 20 | node server.js 21 | ``` 22 | 23 | ### react-app 24 | 25 | ``` 26 | yarn install 27 | yarn start 28 | ``` 29 | 30 | 31 | 32 | ## Structure 33 | 34 | **프로토콜 버퍼**(Protocol Buffers)는 구조화된 데이터를 [직렬화](https://ko.wikipedia.org/wiki/직렬화)하는 방식이다. 유선이나 데이터 저장을 목적으로 서로 통신할 프로그램을 개발할 때 유용하다. proto파일을 grpc-web용으로 컴파일한 뒤, 아래와 같이 불러온다. 이것으로 서버에 연결하고, 스키마를 불러온다. 35 | 36 | ```javascript 37 | const { ChatClient } = require('../protos/chat_grpc_web_pb'); 38 | const { Message } = require('../protos/chat_pb.js'); 39 | 40 | var client= new ChatClient('http://localhost:9090', null, null); 41 | ``` 42 | 43 | grpc-web 기준으로, 프로토파일을 컴파일 하는 명령어는 다음과 같다. 이 과정을 거치면, proto 파일명을 가진 `[proto file name]_grpc_web_pb.js`, `[proto file name]_pb.js` 두 개의 파일이 생성될 것이다. 44 | 45 | ```protobuf 46 | syntax = "proto3"; //Specify proto3 version. 47 | 48 | package example; //Optional: unique package name. 49 | 50 | service Chat { //Service class to be used by the clients 51 | rpc join(Message) returns (stream Message){} 52 | rpc send(Message) returns (Message){} 53 | } 54 | 55 | message Message { //Information that will be passed between client and service 56 | string user = 1; 57 | string text = 2; 58 | } 59 | ``` 60 | 61 | ```bash 62 | protoc -I=. [protofile] --js_out=import_style=commonjs:. --grpc-web_out=import_style=commonjs,mode=grpcwebtext:. 63 | ``` 64 | 65 | react hook 중 하나인 useEffect를 이용해 컴포넌트가 마운트 될 때 stream 객체에 이벤트리스너를 붙여준다. `stream.on('data')` 는 스트림객체에서 데이터를 받을 때마다 두번째 인자로 콜백함수를 실행시킨다. 66 | 67 | ```javascript 68 | useEffect(() => { 69 | let streamRequest = new Message(); 70 | streamRequest.setUser("user"); 71 | 72 | var stream = client.join( 73 | streamRequest, 74 | null 75 | ); 76 | 77 | stream.on('data', function(response) { 78 | console.log(response); 79 | setChat(c => [...c, { name : response.array[0], msg : response.array[1] }]); 80 | }); 81 | 82 | return () => { 83 | }; 84 | 85 | },[]); 86 | ``` 87 | 88 | 메시지를 보낼 때는 proto파일에서 만들었던 스키마를 불러와서, 객체를 만들어준다. 그 뒤로, set을 통하여 그 객체에 정보를 담는다. 아래의 코드와 같다. 89 | 90 | ```javascript 91 | const request = new Message(); 92 | request.setText(msg); 93 | request.setUser(name); 94 | client.send(request, {}, (err, response) => { 95 | if (response == null) { 96 | console.log(err) 97 | }else { 98 | console.log(response) 99 | } 100 | }); 101 | ``` 102 | 103 | https://github.com/grpc/grpc-web/issues/347 104 | 105 | gRPC web-client won’t send HTTP2 requests. Instead, you need a proxy between your web-client and gRPC backend service for converting that HTTP1 request to HTTP2. gRPC web client has built-in support for Envoy as a proxy. You can find more information about this [here](https://grpc.io/blog/state-of-grpc-web#f2). 106 | 107 | gRPC web-client는 http2 request를 보내지 않는다. 대신 proxy를 web-client와 gRPC 백엔드 서버 사이에 둠으로써 http1 요청을 http2 요청으로 변환한다. gRPC web-client는 Envoy를 프록시로서 기본적으로 지원한다. 따라서 아래의 명령어대로 DockerFile을 build하고 run 한다. 108 | 109 | ``` 110 | docker build -t parkgeonhu/envoy . 111 | ``` 112 | 113 | ``` 114 | docker run -d -p 9090:9090 parkgeonhu/envoy 115 | ``` 116 | 117 | 다음은 node.js 기반 gRPC 백엔드 서버 코드다. 이것은 `node server.js`로 실행시켜주도록 한다. 이제 클라이언트에서 메시지를 보내면 envoy proxy server를 거쳐서 이 서버로 넘어오게 될 것이다. 서버로 넘어오게 되면, ServerWritableStream 이 넘어온다. 이것을 이용해 server side streaming이 가능하다. notifyChat 함수를 보면 `user.write` 를 통해 메시지를 전달하고 있다. 118 | 119 | ```javascript 120 | let grpc = require("grpc"); 121 | var protoLoader = require("@grpc/proto-loader"); 122 | 123 | const server = new grpc.Server(); 124 | const SERVER_ADDRESS = "localhost:8080"; 125 | 126 | // Load protobuf 127 | let proto = grpc.loadPackageDefinition( 128 | protoLoader.loadSync("src/protos/chat.proto", { 129 | keepCase: true, 130 | longs: String, 131 | enums: String, 132 | defaults: true, 133 | oneofs: true 134 | }) 135 | ); 136 | 137 | let users = []; 138 | 139 | // Receive message from client joining 140 | function join(call, callback) { 141 | console.log(call) 142 | users.push(call); 143 | notifyChat({ user: "Server", text: "new user joined ..." }); 144 | } 145 | 146 | // Receive message from client 147 | function send(call, callback) { 148 | console.log(call.request); 149 | 150 | notifyChat(call.request); 151 | console.log(callback) 152 | return callback(null,{text:call.request.text}) 153 | } 154 | 155 | // Send message to all connected clients 156 | function notifyChat(message) { 157 | users.forEach(user => { 158 | user.write(message); 159 | }); 160 | } 161 | 162 | // Define server with the methods and start it 163 | server.addService(proto.example.Chat.service, { join: join, send: send }); 164 | 165 | server.bind(SERVER_ADDRESS, grpc.ServerCredentials.createInsecure()); 166 | 167 | console.log("Start Server!"); 168 | 169 | server.start(); 170 | ``` 171 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/protos/chat_pb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * @enhanceable 4 | * @suppress {messageConventions} JS Compiler reports an error if a variable or 5 | * field starts with 'MSG_' and isn't a translatable message. 6 | * @public 7 | */ 8 | // GENERATED CODE -- DO NOT EDIT! 9 | /* eslint-disable */ 10 | 11 | var jspb = require('google-protobuf'); 12 | var goog = jspb; 13 | var global = Function('return this')(); 14 | 15 | goog.exportSymbol('proto.example.Message', null, global); 16 | 17 | /** 18 | * Generated by JsPbCodeGenerator. 19 | * @param {Array=} opt_data Optional initial data array, typically from a 20 | * server response, or constructed directly in Javascript. The array is used 21 | * in place and becomes part of the constructed object. It is not cloned. 22 | * If no data is provided, the constructed object will be empty, but still 23 | * valid. 24 | * @extends {jspb.Message} 25 | * @constructor 26 | */ 27 | proto.example.Message = function(opt_data) { 28 | jspb.Message.initialize(this, opt_data, 0, -1, null, null); 29 | }; 30 | goog.inherits(proto.example.Message, jspb.Message); 31 | if (goog.DEBUG && !COMPILED) { 32 | proto.example.Message.displayName = 'proto.example.Message'; 33 | } 34 | 35 | 36 | if (jspb.Message.GENERATE_TO_OBJECT) { 37 | /** 38 | * Creates an object representation of this proto suitable for use in Soy templates. 39 | * Field names that are reserved in JavaScript and will be renamed to pb_name. 40 | * To access a reserved field use, foo.pb_, eg, foo.pb_default. 41 | * For the list of reserved names please see: 42 | * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. 43 | * @param {boolean=} opt_includeInstance Whether to include the JSPB instance 44 | * for transitional soy proto support: http://goto/soy-param-migration 45 | * @return {!Object} 46 | */ 47 | proto.example.Message.prototype.toObject = function(opt_includeInstance) { 48 | return proto.example.Message.toObject(opt_includeInstance, this); 49 | }; 50 | 51 | 52 | /** 53 | * Static version of the {@see toObject} method. 54 | * @param {boolean|undefined} includeInstance Whether to include the JSPB 55 | * instance for transitional soy proto support: 56 | * http://goto/soy-param-migration 57 | * @param {!proto.example.Message} msg The msg instance to transform. 58 | * @return {!Object} 59 | * @suppress {unusedLocalVariables} f is only used for nested messages 60 | */ 61 | proto.example.Message.toObject = function(includeInstance, msg) { 62 | var f, obj = { 63 | user: jspb.Message.getFieldWithDefault(msg, 1, ""), 64 | text: jspb.Message.getFieldWithDefault(msg, 2, "") 65 | }; 66 | 67 | if (includeInstance) { 68 | obj.$jspbMessageInstance = msg; 69 | } 70 | return obj; 71 | }; 72 | } 73 | 74 | 75 | /** 76 | * Deserializes binary data (in protobuf wire format). 77 | * @param {jspb.ByteSource} bytes The bytes to deserialize. 78 | * @return {!proto.example.Message} 79 | */ 80 | proto.example.Message.deserializeBinary = function(bytes) { 81 | var reader = new jspb.BinaryReader(bytes); 82 | var msg = new proto.example.Message; 83 | return proto.example.Message.deserializeBinaryFromReader(msg, reader); 84 | }; 85 | 86 | 87 | /** 88 | * Deserializes binary data (in protobuf wire format) from the 89 | * given reader into the given message object. 90 | * @param {!proto.example.Message} msg The message object to deserialize into. 91 | * @param {!jspb.BinaryReader} reader The BinaryReader to use. 92 | * @return {!proto.example.Message} 93 | */ 94 | proto.example.Message.deserializeBinaryFromReader = function(msg, reader) { 95 | while (reader.nextField()) { 96 | if (reader.isEndGroup()) { 97 | break; 98 | } 99 | var field = reader.getFieldNumber(); 100 | switch (field) { 101 | case 1: 102 | var value = /** @type {string} */ (reader.readString()); 103 | msg.setUser(value); 104 | break; 105 | case 2: 106 | var value = /** @type {string} */ (reader.readString()); 107 | msg.setText(value); 108 | break; 109 | default: 110 | reader.skipField(); 111 | break; 112 | } 113 | } 114 | return msg; 115 | }; 116 | 117 | 118 | /** 119 | * Serializes the message to binary data (in protobuf wire format). 120 | * @return {!Uint8Array} 121 | */ 122 | proto.example.Message.prototype.serializeBinary = function() { 123 | var writer = new jspb.BinaryWriter(); 124 | proto.example.Message.serializeBinaryToWriter(this, writer); 125 | return writer.getResultBuffer(); 126 | }; 127 | 128 | 129 | /** 130 | * Serializes the given message to binary data (in protobuf wire 131 | * format), writing to the given BinaryWriter. 132 | * @param {!proto.example.Message} message 133 | * @param {!jspb.BinaryWriter} writer 134 | * @suppress {unusedLocalVariables} f is only used for nested messages 135 | */ 136 | proto.example.Message.serializeBinaryToWriter = function(message, writer) { 137 | var f = undefined; 138 | f = message.getUser(); 139 | if (f.length > 0) { 140 | writer.writeString( 141 | 1, 142 | f 143 | ); 144 | } 145 | f = message.getText(); 146 | if (f.length > 0) { 147 | writer.writeString( 148 | 2, 149 | f 150 | ); 151 | } 152 | }; 153 | 154 | 155 | /** 156 | * optional string user = 1; 157 | * @return {string} 158 | */ 159 | proto.example.Message.prototype.getUser = function() { 160 | return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); 161 | }; 162 | 163 | 164 | /** @param {string} value */ 165 | proto.example.Message.prototype.setUser = function(value) { 166 | jspb.Message.setProto3StringField(this, 1, value); 167 | }; 168 | 169 | 170 | /** 171 | * optional string text = 2; 172 | * @return {string} 173 | */ 174 | proto.example.Message.prototype.getText = function() { 175 | return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); 176 | }; 177 | 178 | 179 | /** @param {string} value */ 180 | proto.example.Message.prototype.setText = function(value) { 181 | jspb.Message.setProto3StringField(this, 2, value); 182 | }; 183 | 184 | 185 | goog.object.extend(exports, proto.example); 186 | -------------------------------------------------------------------------------- /src/protos/chat_grpc_web_pb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview gRPC-Web generated client stub for example 3 | * @enhanceable 4 | * @public 5 | */ 6 | 7 | // GENERATED CODE -- DO NOT EDIT! 8 | 9 | 10 | /* eslint-disable */ 11 | // @ts-nocheck 12 | 13 | 14 | 15 | const grpc = {}; 16 | grpc.web = require('grpc-web'); 17 | 18 | const proto = {}; 19 | proto.example = require('./chat_pb.js'); 20 | 21 | /** 22 | * @param {string} hostname 23 | * @param {?Object} credentials 24 | * @param {?Object} options 25 | * @constructor 26 | * @struct 27 | * @final 28 | */ 29 | proto.example.ChatClient = 30 | function(hostname, credentials, options) { 31 | if (!options) options = {}; 32 | options['format'] = 'text'; 33 | 34 | /** 35 | * @private @const {!grpc.web.GrpcWebClientBase} The client 36 | */ 37 | this.client_ = new grpc.web.GrpcWebClientBase(options); 38 | 39 | /** 40 | * @private @const {string} The hostname 41 | */ 42 | this.hostname_ = hostname; 43 | 44 | }; 45 | 46 | 47 | /** 48 | * @param {string} hostname 49 | * @param {?Object} credentials 50 | * @param {?Object} options 51 | * @constructor 52 | * @struct 53 | * @final 54 | */ 55 | proto.example.ChatPromiseClient = 56 | function(hostname, credentials, options) { 57 | if (!options) options = {}; 58 | options['format'] = 'text'; 59 | 60 | /** 61 | * @private @const {!grpc.web.GrpcWebClientBase} The client 62 | */ 63 | this.client_ = new grpc.web.GrpcWebClientBase(options); 64 | 65 | /** 66 | * @private @const {string} The hostname 67 | */ 68 | this.hostname_ = hostname; 69 | 70 | }; 71 | 72 | 73 | /** 74 | * @const 75 | * @type {!grpc.web.MethodDescriptor< 76 | * !proto.example.Message, 77 | * !proto.example.Message>} 78 | */ 79 | const methodDescriptor_Chat_join = new grpc.web.MethodDescriptor( 80 | '/example.Chat/join', 81 | grpc.web.MethodType.SERVER_STREAMING, 82 | proto.example.Message, 83 | proto.example.Message, 84 | /** 85 | * @param {!proto.example.Message} request 86 | * @return {!Uint8Array} 87 | */ 88 | function(request) { 89 | return request.serializeBinary(); 90 | }, 91 | proto.example.Message.deserializeBinary 92 | ); 93 | 94 | 95 | /** 96 | * @const 97 | * @type {!grpc.web.AbstractClientBase.MethodInfo< 98 | * !proto.example.Message, 99 | * !proto.example.Message>} 100 | */ 101 | const methodInfo_Chat_join = new grpc.web.AbstractClientBase.MethodInfo( 102 | proto.example.Message, 103 | /** 104 | * @param {!proto.example.Message} request 105 | * @return {!Uint8Array} 106 | */ 107 | function(request) { 108 | return request.serializeBinary(); 109 | }, 110 | proto.example.Message.deserializeBinary 111 | ); 112 | 113 | 114 | /** 115 | * @param {!proto.example.Message} request The request proto 116 | * @param {?Object} metadata User defined 117 | * call metadata 118 | * @return {!grpc.web.ClientReadableStream} 119 | * The XHR Node Readable Stream 120 | */ 121 | proto.example.ChatClient.prototype.join = 122 | function(request, metadata) { 123 | return this.client_.serverStreaming(this.hostname_ + 124 | '/example.Chat/join', 125 | request, 126 | metadata || {}, 127 | methodDescriptor_Chat_join); 128 | }; 129 | 130 | 131 | /** 132 | * @param {!proto.example.Message} request The request proto 133 | * @param {?Object} metadata User defined 134 | * call metadata 135 | * @return {!grpc.web.ClientReadableStream} 136 | * The XHR Node Readable Stream 137 | */ 138 | proto.example.ChatPromiseClient.prototype.join = 139 | function(request, metadata) { 140 | return this.client_.serverStreaming(this.hostname_ + 141 | '/example.Chat/join', 142 | request, 143 | metadata || {}, 144 | methodDescriptor_Chat_join); 145 | }; 146 | 147 | 148 | /** 149 | * @const 150 | * @type {!grpc.web.MethodDescriptor< 151 | * !proto.example.Message, 152 | * !proto.example.Message>} 153 | */ 154 | const methodDescriptor_Chat_send = new grpc.web.MethodDescriptor( 155 | '/example.Chat/send', 156 | grpc.web.MethodType.UNARY, 157 | proto.example.Message, 158 | proto.example.Message, 159 | /** 160 | * @param {!proto.example.Message} request 161 | * @return {!Uint8Array} 162 | */ 163 | function(request) { 164 | return request.serializeBinary(); 165 | }, 166 | proto.example.Message.deserializeBinary 167 | ); 168 | 169 | 170 | /** 171 | * @const 172 | * @type {!grpc.web.AbstractClientBase.MethodInfo< 173 | * !proto.example.Message, 174 | * !proto.example.Message>} 175 | */ 176 | const methodInfo_Chat_send = new grpc.web.AbstractClientBase.MethodInfo( 177 | proto.example.Message, 178 | /** 179 | * @param {!proto.example.Message} request 180 | * @return {!Uint8Array} 181 | */ 182 | function(request) { 183 | return request.serializeBinary(); 184 | }, 185 | proto.example.Message.deserializeBinary 186 | ); 187 | 188 | 189 | /** 190 | * @param {!proto.example.Message} request The 191 | * request proto 192 | * @param {?Object} metadata User defined 193 | * call metadata 194 | * @param {function(?grpc.web.Error, ?proto.example.Message)} 195 | * callback The callback function(error, response) 196 | * @return {!grpc.web.ClientReadableStream|undefined} 197 | * The XHR Node Readable Stream 198 | */ 199 | proto.example.ChatClient.prototype.send = 200 | function(request, metadata, callback) { 201 | return this.client_.rpcCall(this.hostname_ + 202 | '/example.Chat/send', 203 | request, 204 | metadata || {}, 205 | methodDescriptor_Chat_send, 206 | callback); 207 | }; 208 | 209 | 210 | /** 211 | * @param {!proto.example.Message} request The 212 | * request proto 213 | * @param {?Object} metadata User defined 214 | * call metadata 215 | * @return {!Promise} 216 | * A native promise that resolves to the response 217 | */ 218 | proto.example.ChatPromiseClient.prototype.send = 219 | function(request, metadata) { 220 | return this.client_.unaryCall(this.hostname_ + 221 | '/example.Chat/send', 222 | request, 223 | metadata || {}, 224 | methodDescriptor_Chat_send); 225 | }; 226 | 227 | 228 | module.exports = proto.example; 229 | 230 | --------------------------------------------------------------------------------