├── 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 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chat application using gRPC, React
2 |
3 | 
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 |
--------------------------------------------------------------------------------