diff --git a/appengine/websockets/README.md b/appengine/websockets/README.md new file mode 100644 index 0000000000..d2cb521648 --- /dev/null +++ b/appengine/websockets/README.md @@ -0,0 +1,54 @@ +# Node.js websockets sample for Google App Engine + +This sample demonstrates how to use websockets on +[Google App Engine Flexible Environment][appengine] with Node.js. + +* [Setup](#setup) +* [Running locally](#running-locally) +* [Deploying to App Engine](#deploying-to-app-engine) +* [Running the tests](#running-the-tests) + +## Setup + +Before you can run or deploy the sample, you need to do the following: + +1. Refer to the [appengine/README.md][readme] file for instructions on + running and deploying. +1. Install dependencies: + + With `npm`: + + npm install + + or with `yarn`: + + yarn install + +## Running locally + +With `npm`: + + npm start + +or with `yarn`: + + yarn start + +## Deploying to App Engine + +With `npm`: + + npm run deploy + +or with `yarn`: + + yarn run deploy + +## Running the tests + +See [Contributing][contributing]. + +[appengine]: https://cloud.google.com/appengine/docs/flexible/nodejs +[readme]: ../README.md +[contributing]: https://github.com/GoogleCloudPlatform/nodejs-docs-samples/blob/master/CONTRIBUTING.md + diff --git a/appengine/websockets/app.js b/appengine/websockets/app.js new file mode 100644 index 0000000000..b85c136a85 --- /dev/null +++ b/appengine/websockets/app.js @@ -0,0 +1,42 @@ +/** + * Copyright 2018, Google LLC. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +// [START appengine_websockets_app] +const app = require('express')(); +app.set('view engine', 'pug'); + +const server = require('http').Server(app); +const io = require('socket.io')(server); + +app.get('/', (req, res) => { + res.render('index.pug'); +}); + +io.on('connection', socket => { + socket.on('chat message', msg => { + io.emit('chat message', msg); + }); +}); + +if (module === require.main) { + const PORT = process.env.PORT || 8080; + server.listen(PORT, () => { + console.log(`App listening on port ${PORT}`); + console.log('Press Ctrl+C to quit.'); + }); +} +// [END appengine_websockets_app] diff --git a/appengine/websockets/app.yaml b/appengine/websockets/app.yaml new file mode 100644 index 0000000000..78189a14fe --- /dev/null +++ b/appengine/websockets/app.yaml @@ -0,0 +1,14 @@ +# [START appengine_websockets_yaml] +runtime: nodejs +env: flex + +# Use only a single instance, so that this local-memory-only chat app will work +# consistently with multiple users. To work across multiple instances, an +# extra-instance messaging system or data store would be needed. +manual_scaling: + instances: 1 + +network: + session_affinity: true +# [END appengine_websockets_yaml] + diff --git a/appengine/websockets/package.json b/appengine/websockets/package.json new file mode 100644 index 0000000000..589340cf41 --- /dev/null +++ b/appengine/websockets/package.json @@ -0,0 +1,37 @@ +{ + "name": "appengine-websockets", + "description": "Node.js websockets sample for Google App Engine", + "version": "0.0.1", + "private": true, + "license": "Apache Version 2.0", + "author": "Google Inc.", + "engines": { + "node": ">=8" + }, + "scripts": { + "deploy": "gcloud app deploy", + "start": "node app.js", + "lint": "samples lint", + "pretest": "npm run lint", + "test": "node app.js & ava -T 30s test/*.js; killall node", + "e2e-test": "samples test deploy" + }, + "dependencies": { + "express": "4.15.4", + "pug": "2.0.0-rc.3", + "socket.io": "2.0.3" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "1.4.17", + "puppeteer": "^1.11.0" + }, + "cloud-repo-tools": { + "test": { + "app": { + "msg": "messages" + } + }, + "requiresKeyFile": true, + "requiresProjectId": true + } +} diff --git a/appengine/websockets/test/index.test.js b/appengine/websockets/test/index.test.js new file mode 100644 index 0000000000..c333f91b31 --- /dev/null +++ b/appengine/websockets/test/index.test.js @@ -0,0 +1,50 @@ +/** + * Copyright 2018, Google LLC. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/* eslint node/no-extraneous-require: "off" */ + +const test = require('ava'); +const puppeteer = require('puppeteer'); +/* global document */ + +let browser, browserPage; + +test.before(async () => { + browser = await puppeteer.launch(); + browserPage = await browser.newPage(); +}); + +test.after.always(async () => { + await browser.close(); +}); + +test('should process chat message', async t => { + await browserPage.goto('http://localhost:8080'); + + await browserPage.evaluate(() => { + document.querySelector('input').value = 'test'; + document.querySelector('button').click(); + }); + + await new Promise(resolve => setTimeout(resolve, 100)); + + const itemText = await browserPage.evaluate( + () => document.querySelector('li').textContent + ); + + t.is(itemText, 'test'); +}); diff --git a/appengine/websockets/views/index.pug b/appengine/websockets/views/index.pug new file mode 100644 index 0000000000..142ecb72b8 --- /dev/null +++ b/appengine/websockets/views/index.pug @@ -0,0 +1,58 @@ +//- Copyright 2018 Google LLC. +//- +//- Licensed under the Apache License, Version 2.0 (the "License"); +//- you may not use this file except in compliance with the License. +//- You may obtain a copy of the License at +//- +//- http://www.apache.org/licenses/LICENSE-2.0 +//- +//- Unless required by applicable law or agreed to in writing, software +//- distributed under the License is distributed on an "AS IS" BASIS, +//- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +//- See the License for the specific language governing permissions and +//- limitations under the License. + +//- [START appengine_websockets_index] +doctype html +html(lang="en") + head + title Socket.IO chat on App Engine + meta(charset="utf-8") + style. + * { margin: 0; padding: 0; box-sizing: border-box; } + body { font: 13px Helvetica, Arial; } + form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; } + form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; } + form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; } + #messages { list-style-type: none; margin: 0; padding: 0; } + #messages li { padding: 5px 10px; } + #messages li:nth-child(odd) { background: #eee; } + //- [START appengine_websockets_form] + body + ul(id="messages") + form(action="") + input(id="m" autocomplete="off") + button Send + //- [END appengine_websockets_form] + + script(src="/socket.io/socket.io.js") + script(src="https://code.jquery.com/jquery-1.11.1.js") + script. + // [START appengine_websockets_js] + $(function () { + var socket = io(); + $('form').submit(function(){ + console.log($('#m').val()); + socket.emit('chat message', $('#m').val()); + $('#m').val(''); + return false; + }); + socket.on('chat message', function(msg){ + console.log(msg); + $('#messages').append($('
  • ').text(msg)); + window.scrollTo(0, document.body.scrollHeight); + }); + }); + // [END appengine_websockets_js] +//- [END appengine_websockets_index] +