From 097ba20190a34a35a14615d81a31cb8a7541d41d Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 21:46:50 -0300 Subject: [PATCH 01/72] . --- .github/workflows/main.yml | 138 ++++++++ playground/serverless/index.html | 480 +++++++++++++++++++++++++++ playground/serverless/worker.mjs | 538 +++++++++++++++++++++++++++++++ 3 files changed, 1156 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 playground/serverless/index.html create mode 100644 playground/serverless/worker.mjs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..1ff00093 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,138 @@ +name: Deploy + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy + steps: + - uses: actions/checkout@v2 + - name: Publish + env: + ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} + AUTH_TOKEN: ${{ secrets.CF_API_TOKEN }} + SCRIPT_NAME: "edge-game" + run: | + set -euo pipefail + + if ! which curl >/dev/null; then + echo "$0: please install curl" >&2 + exit 1 + fi + + if ! which jq >/dev/null; then + echo "$0: please install jq" >&2 + exit 1 + fi + + ACCOUNT_ID=$ACCOUNT_ID + AUTH_TOKEN=$AUTH_TOKEN + SCRIPT_NAME=$SCRIPT_NAME + + # curl_api performs a curl command passing the appropriate authorization headers, and parses the + # JSON response for errors. In case of errors, exit. Otherwise, write just the result part to + # stdout. + curl_api() { + RESULT=$(curl -s -H "Authorization: Bearer $AUTH_TOKEN" "$@") + if [ $(echo "$RESULT" | jq .success) = true ]; then + echo "$RESULT" | jq .result + return 0 + else + echo "API ERROR:" >&2 + echo "$RESULT" >&2 + return 1 + fi + } + + # Let's verify the credentials work by listing Workers scripts and Durable Object namespaces. If + # either of these requests error then we're certainly not going to be able to continue. + echo "Checking if credentials can access Workers..." + curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts >/dev/null + echo "Checking if credentials can access Durable Objects..." + curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/durable_objects/namespaces >/dev/null + echo "Credentials OK! Publishing..." + + # upload_script uploads our Worker code with the appropriate metadata. + upload_script() { + curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME \ + -X PUT \ + -F "metadata=@metadata.json;type=application/json" \ + -F "script=@playground/serverless/worker.mjs;type=application/javascript+module" \ + -F "html=@playground/serverless/index.html;type=application/octet-stream" > /dev/null + } + + # upload_bootstrap_script is a temporary hack to work around a chicken-and-egg problem: in order + # to define a Durable Object namespace, we must tell it a script and class name. But when we upload + # our script, we need to configure the environment to bind to our durable object namespaces. This + # function uploads a version of our script with an empty environment (no bindings). The script won't + # be able to run correctly, but this gets us far enough to define the namespaces, and then we can + # upload the script with full environment later. + # + # This is obviously dumb and we (Cloudflare) will come up with something better soon. + upload_bootstrap_script() { + echo '{"main_module": "playground/serverless/worker.mjs"}' > bootstrap-metadata.json + curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME \ + -X PUT \ + -F "metadata=@bootstrap-metadata.json;type=application/json" \ + -F "script=@playground/serverless/worker.mjs;type=application/javascript+module" \ + -F "html=@playground/serverless/index.html;type=application/octet-stream" > /dev/null + rm bootstrap-metadata.json + } + + # upsert_namespace configures a Durable Object namespace so that instances of it can be created + # and called from other scripts (or from the same script). This function checks if the namespace + # already exists, creates it if it doesn't, and either way writes the namespace ID to stdout. + # + # The namespace ID can be used to configure environment bindings in other scripts (or even the same + # script) such that they can send messages to instances of this namespace. + upsert_namespace() { + # Check if the namespace exists already. + EXISTING_ID=$(\ + curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/durable_objects/namespaces | \ + jq -r ".[] | select(.script == \"$SCRIPT_NAME\" and .class == \"$1\") | .id") + + if [ "$EXISTING_ID" != "" ]; then + echo $EXISTING_ID + return + fi + + # No. Create it. + curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/durable_objects/namespaces \ + -X POST --data "{\"name\": \"$SCRIPT_NAME-$1\", \"script\": \"$SCRIPT_NAME\", \"class\": \"$1\"}" | \ + jq -r .id + } + + if [ ! -e metadata.json ]; then + # If metadata.json doesn't exist we assume this is first-time setup and we need to create the + # namespaces. + upload_bootstrap_script + ROOMS_ID=$(upsert_namespace ChatRoom) + LIMITERS_ID=$(upsert_namespace RateLimiter) + + cat > metadata.json << __EOF__ + { + "main_module": "playground/serverless/worker.mjs", + "bindings": [ + { + "type": "durable_object_namespace", + "name": "rooms", + "namespace_id": "$ROOMS_ID" + }, + { + "type": "durable_object_namespace", + "name": "limiters", + "namespace_id": "$LIMITERS_ID" + } + ] + } + __EOF__ + fi + + upload_script + + echo "App uploaded to your account under the name: $SCRIPT_NAME" + echo "You may deploy it to a specific host in the Cloudflare Dashboard." diff --git a/playground/serverless/index.html b/playground/serverless/index.html new file mode 100644 index 00000000..50adb3c1 --- /dev/null +++ b/playground/serverless/index.html @@ -0,0 +1,480 @@ + + + + + + + + + + + + + + + +
+ +

This chat runs entirely on the edge, powered by
+ Cloudflare Workers Durable Objects

+
+
+

Enter a public room:

+ +

OR

+ +
+
+
+
+
+
+ +
+ + + + + + + + \ No newline at end of file diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs new file mode 100644 index 00000000..a6cc690d --- /dev/null +++ b/playground/serverless/worker.mjs @@ -0,0 +1,538 @@ +// This is the Edge Chat Demo Worker, built using Durable Objects! + +// =============================== +// Introduction to Modules +// =============================== +// +// The first thing you might notice, if you are familiar with the Workers platform, is that this +// Worker is written differently from others you may have seen. It even has a different file +// extension. The `mjs` extension means this JavaScript is an ES Module, which, among other things, +// means it has imports and exports. Unlike other Workers, this code doesn't use +// `addEventListener("fetch", handler)` to register its main HTTP handler; instead, it _exports_ +// a handler, as we'll see below. +// +// This is a new way of writing Workers that we expect to introduce more broadly in the future. We +// like this syntax because it is *composable*: You can take two workers written this way and +// merge them into one worker, by importing the two Workers' exported handlers yourself, and then +// exporting a new handler that call into the other Workers as appropriate. +// +// This new syntax is required when using Durable Objects, because your Durable Objects are +// implemented by classes, and those classes need to be exported. The new syntax can be used for +// writing regular Workers (without Durable Objects) too, but for now, you must be in the Durable +// Objects beta to be able to use the new syntax, while we work out the quirks. +// +// To see the API for uploading module-based Workers, check out the publish.sh script. + +// =============================== +// Required Environment +// =============================== +// +// This worker, when deployed, must be configured with two environment bindings: +// * rooms: A Durable Object namespace binding mapped to the ChatRoom class. +// * limiters: A Durable Object namespace binding mapped to the RateLimiter class. +// +// Incidentally, in pre-modules Workers syntax, "bindings" (like KV bindings, secrets, etc.) +// appeared in your script as global variables, but in the new modules syntax, this is no longer +// the case. Instead, bindings are now delivered in an "environment object" when an event handler +// (or Durable Object class constructor) is called. Look for the variable `env` below. +// +// We made this change, again, for composability: The global scope is global, but if you want to +// call into existing code that has different environment requirements, then you need to be able +// to pass the environment as a parameter instead. +// +// Once again, see the publish.sh script to understand how the environment is configured. + +// ======================================================================================= +// The regular Worker part... +// +// This section of the code implements a normal Worker that receives HTTP requests from external +// clients. This part is stateless. + +// With the introduction of modules, we're experimenting with allowing text/data blobs to be +// uploaded and exposed as synthetic modules. We uploaded `chat.html` as a module of type +// `application/octet-stream`, i.e. just a byte blob. So when we import it as `HTML` here, we get +// the HTML content as an `ArrayBuffer`. So, we can serve our app's static asset without relying on +// any separate storage. (However, the space available for assets served this way is very limited; +// larger sites should continue to use Workers KV to serve assets.) +import HTML from "chat.html"; + +// `handleErrors()` is a little utility function that can wrap an HTTP request handler in a +// try/catch and return errors to the client. You probably wouldn't want to use this in production +// code but it is convenient when debugging and iterating. +async function handleErrors(request, func) { + try { + return await func(); + } catch (err) { + if (request.headers.get("Upgrade") == "websocket") { + // Annoyingly, if we return an HTTP error in response to a WebSocket request, Chrome devtools + // won't show us the response body! So... let's send a WebSocket response with an error + // frame instead. + let pair = new WebSocketPair(); + pair[1].accept(); + pair[1].send(JSON.stringify({error: err.stack})); + pair[1].close(1011, "Uncaught exception during session setup"); + return new Response(null, { status: 101, webSocket: pair[0] }); + } else { + return new Response(err.stack, {status: 500}); + } + } +} + +// In modules-syntax workers, we use `export default` to export our script's main event handlers. +// Here, we export one handler, `fetch`, for receiving HTTP requests. In pre-modules workers, the +// fetch handler was registered using `addEventHandler("fetch", event => { ... })`; this is just +// new syntax for essentially the same thing. +// +// `fetch` isn't the only handler. If your worker runs on a Cron schedule, it will receive calls +// to a handler named `scheduled`, which should be exported here in a similar way. We will be +// adding other handlers for other types of events over time. +export default { + async fetch(request, env) { + return await handleErrors(request, async () => { + // We have received an HTTP request! Parse the URL and route the request. + + let url = new URL(request.url); + let path = url.pathname.slice(1).split('/'); + + if (!path[0]) { + // Serve our HTML at the root path. + return new Response(HTML, {headers: {"Content-Type": "text/html;charset=UTF-8"}}); + } + + switch (path[0]) { + case "api": + // This is a request for `/api/...`, call the API handler. + return handleApiRequest(path.slice(1), request, env); + + default: + return new Response("Not found", {status: 404}); + } + }); + } +} + + +async function handleApiRequest(path, request, env) { + // We've received at API request. Route the request based on the path. + + switch (path[0]) { + case "room": { + // Request for `/api/room/...`. + + if (!path[1]) { + // The request is for just "/api/room", with no ID. + if (request.method == "POST") { + // POST to /api/room creates a private room. + // + // Incidentally, this code doesn't actually store anything. It just generates a valid + // unique ID for this namespace. Each durable object namespace has its own ID space, but + // IDs from one namespace are not valid for any other. + // + // The IDs returned by `newUniqueId()` are unguessable, so are a valid way to implement + // "anyone with the link can access" sharing. Additionally, IDs generated this way have + // a performance benefit over IDs generated from names: When a unique ID is generated, + // the system knows it is unique without having to communicate with the rest of the + // world -- i.e., there is no way that someone in the UK and someone in New Zealand + // could coincidentally create the same ID at the same time, because unique IDs are, + // well, unique! + let id = env.rooms.newUniqueId(); + return new Response(id.toString(), {headers: {"Access-Control-Allow-Origin": "*"}}); + } else { + // If we wanted to support returning a list of public rooms, this might be a place to do + // it. The list of room names might be a good thing to store in KV, though a singleton + // Durable Object is also a possibility as long as the Cache API is used to cache reads. + // (A caching layer would be needed because a single Durable Object is single-threaded, + // so the amount of traffic it can handle is limited. Also, caching would improve latency + // for users who don't happen to be located close to the singleton.) + // + // For this demo, though, we're not implementing a public room list, mainly because + // inevitably some trolls would probably register a bunch of offensive room names. Sigh. + return new Response("Method not allowed", {status: 405}); + } + } + + // OK, the request is for `/api/room//...`. It's time to route to the Durable Object + // for the specific room. + let name = path[1]; + + // Each Durable Object has a 256-bit unique ID. IDs can be derived from string names, or + // chosen randomly by the system. + let id; + if (name.match(/^[0-9a-f]{64}$/)) { + // The name is 64 hex digits, so let's assume it actually just encodes an ID. We use this + // for private rooms. `idFromString()` simply parses the text as a hex encoding of the raw + // ID (and verifies that this is a valid ID for this namespace). + id = env.rooms.idFromString(name); + } else if (name.length <= 32) { + // Treat as a string room name (limited to 32 characters). `idFromName()` consistently + // derives an ID from a string. + id = env.rooms.idFromName(name); + } else { + return new Response("Name too long", {status: 404}); + } + + // Get the Durable Object stub for this room! The stub is a client object that can be used + // to send messages to the remote Durable Object instance. The stub is returned immediately; + // there is no need to await it. This is important because you would not want to wait for + // a network round trip before you could start sending requests. Since Durable Objects are + // created on-demand when the ID is first used, there's nothing to wait for anyway; we know + // an object will be available somewhere to receive our requests. + let roomObject = env.rooms.get(id); + + // Compute a new URL with `/api/room/` removed. We'll forward the rest of the path + // to the Durable Object. + let newUrl = new URL(request.url); + newUrl.pathname = "/" + path.slice(2).join("/"); + + // Send the request to the object. The `fetch()` method of a Durable Object stub has the + // same signature as the global `fetch()` function, but the request is always sent to the + // object, regardless of the request's URL. + return roomObject.fetch(newUrl, request); + } + + default: + return new Response("Not found", {status: 404}); + } +} + +// ======================================================================================= +// The ChatRoom Durable Object Class + +// ChatRoom implements a Durable Object that coordinates an individual chat room. Participants +// connect to the room using WebSockets, and the room broadcasts messages from each participant +// to all others. +export class ChatRoom { + constructor(controller, env) { + // `controller.storage` provides access to our durable storage. It provides a simple KV + // get()/put() interface. + this.storage = controller.storage; + + // `env` is our environment bindings (discussed earlier). + this.env = env; + + // We will put the WebSocket objects for each client, along with some metadata, into + // `sessions`. + this.sessions = []; + + // We keep track of the last-seen message's timestamp just so that we can assign monotonically + // increasing timestamps even if multiple messages arrive simultaneously (see below). There's + // no need to store this to disk since we assume if the object is destroyed and recreated, much + // more than a millisecond will have gone by. + this.lastTimestamp = 0; + } + + // The system will call fetch() whenever an HTTP request is sent to this Object. Such requests + // can only be sent from other Worker code, such as the code above; these requests don't come + // directly from the internet. In the future, we will support other formats than HTTP for these + // communications, but we started with HTTP for its familiarity. + async fetch(request) { + return await handleErrors(request, async () => { + let url = new URL(request.url); + + switch (url.pathname) { + case "/websocket": { + // The request is to `/api/room//websocket`. A client is trying to establish a new + // WebSocket session. + if (request.headers.get("Upgrade") != "websocket") { + return new Response("expected websocket", {status: 400}); + } + + // Get the client's IP address for use with the rate limiter. + let ip = request.headers.get("CF-Connecting-IP"); + + // To accept the WebSocket request, we create a WebSocketPair (which is like a socketpair, + // i.e. two WebSockets that talk to each other), we return one end of the pair in the + // response, and we operate on the other end. Note that this API is not part of the + // Fetch API standard; unfortunately, the Fetch API / Service Workers specs do not define + // any way to act as a WebSocket server today. + let pair = new WebSocketPair(); + + // We're going to take pair[1] as our end, and return pair[0] to the client. + await this.handleSession(pair[1], ip); + + // Now we return the other end of the pair to the client. + return new Response(null, { status: 101, webSocket: pair[0] }); + } + + default: + return new Response("Not found", {status: 404}); + } + }); + } + + // handleSession() implements our WebSocket-based chat protocol. + async handleSession(webSocket, ip) { + // Accept our end of the WebSocket. This tells the runtime that we'll be terminating the + // WebSocket in JavaScript, not sending it elsewhere. + webSocket.accept(); + + // Set up our rate limiter client. + let limiterId = this.env.limiters.idFromName(ip); + let limiter = new RateLimiterClient( + () => this.env.limiters.get(limiterId), + err => webSocket.close(1011, err.stack)); + + // Create our session and add it to the sessions list. + // We don't send any messages to the client until it has sent us the initial user info + // message. Until then, we will queue messages in `session.blockedMessages`. + let session = {webSocket, blockedMessages: []}; + this.sessions.push(session); + + // Queue "join" messages for all online users, to populate the client's roster. + this.sessions.forEach(otherSession => { + if (otherSession.name) { + session.blockedMessages.push(JSON.stringify({joined: otherSession.name})); + } + }); + + // Load the last 100 messages from the chat history stored on disk, and send them to the + // client. + let storage = await this.storage.list({reverse: true, limit: 100}); + let backlog = [...storage.values()]; + backlog.reverse(); + backlog.forEach(value => { + session.blockedMessages.push(value); + }); + + // Set event handlers to receive messages. + let receivedUserInfo = false; + webSocket.addEventListener("message", async msg => { + try { + if (session.quit) { + // Whoops, when trying to send to this WebSocket in the past, it threw an exception and + // we marked it broken. But somehow we got another message? I guess try sending a + // close(), which might throw, in which case we'll try to send an error, which will also + // throw, and whatever, at least we won't accept the message. (This probably can't + // actually happen. This is defensive coding.) + webSocket.close(1011, "WebSocket broken."); + return; + } + + // Check if the user is over their rate limit and reject the message if so. + if (!limiter.checkLimit()) { + webSocket.send(JSON.stringify({ + error: "Your IP is being rate-limited, please try again later." + })); + return; + } + + // I guess we'll use JSON. + let data = JSON.parse(msg.data); + + if (!receivedUserInfo) { + // The first message the client sends is the user info message with their name. Save it + // into their session object. + session.name = "" + (data.name || "anonymous"); + + // Don't let people use ridiculously long names. (This is also enforced on the client, + // so if they get here they are not using the intended client.) + if (session.name.length > 32) { + webSocket.send(JSON.stringify({error: "Name too long."})); + webSocket.close(1009, "Name too long."); + return; + } + + // Deliver all the messages we queued up since the user connected. + session.blockedMessages.forEach(queued => { + webSocket.send(queued); + }); + delete session.blockedMessages; + + // Broadcast to all other connections that this user has joined. + this.broadcast({joined: session.name}); + + webSocket.send(JSON.stringify({ready: true})); + + // Note that we've now received the user info message. + receivedUserInfo = true; + + return; + } + + // Construct sanitized message for storage and broadcast. + data = { name: session.name, message: "" + data.message }; + + // Block people from sending overly long messages. This is also enforced on the client, + // so to trigger this the user must be bypassing the client code. + if (data.message.length > 256) { + webSocket.send(JSON.stringify({error: "Message too long."})); + return; + } + + // Add timestamp. Here's where this.lastTimestamp comes in -- if we receive a bunch of + // messages at the same time (or if the clock somehow goes backwards????), we'll assign + // them sequential timestamps, so at least the ordering is maintained. + data.timestamp = Math.max(Date.now(), this.lastTimestamp + 1); + this.lastTimestamp = data.timestamp; + + // Broadcast the message to all other WebSockets. + let dataStr = JSON.stringify(data); + this.broadcast(dataStr); + + // Save message. + let key = new Date(data.timestamp).toISOString(); + await this.storage.put(key, dataStr); + } catch (err) { + // Report any exceptions directly back to the client. As with our handleErrors() this + // probably isn't what you'd want to do in production, but it's convenient when testing. + webSocket.send(JSON.stringify({error: err.stack})); + } + }); + + // On "close" and "error" events, remove the WebSocket from the sessions list and broadcast + // a quit message. + let closeOrErrorHandler = evt => { + session.quit = true; + this.sessions = this.sessions.filter(member => member !== session); + if (session.name) { + this.broadcast({quit: session.name}); + } + }; + webSocket.addEventListener("close", closeOrErrorHandler); + webSocket.addEventListener("error", closeOrErrorHandler); + } + + // broadcast() broadcasts a message to all clients. + broadcast(message) { + // Apply JSON if we weren't given a string to start with. + if (typeof message !== "string") { + message = JSON.stringify(message); + } + + // Iterate over all the sessions sending them messages. + let quitters = []; + this.sessions = this.sessions.filter(session => { + if (session.name) { + try { + session.webSocket.send(message); + return true; + } catch (err) { + // Whoops, this connection is dead. Remove it from the list and arrange to notify + // everyone below. + session.quit = true; + quitters.push(session); + return false; + } + } else { + // This session hasn't sent the initial user info message yet, so we're not sending them + // messages yet (no secret lurking!). Queue the message to be sent later. + session.blockedMessages.push(message); + return true; + } + }); + + quitters.forEach(quitter => { + if (quitter.name) { + this.broadcast({quit: quitter.name}); + } + }); + } +} + +// ======================================================================================= +// The RateLimiter Durable Object class. + +// RateLimiter implements a Durable Object that tracks the frequency of messages from a particular +// source and decides when messages should be dropped because the source is sending too many +// messages. +// +// We utilize this in ChatRoom, above, to apply a per-IP-address rate limit. These limits are +// global, i.e. they apply across all chat rooms, so if a user spams one chat room, they will find +// themselves rate limited in all other chat rooms simultaneously. +export class RateLimiter { + constructor(controller, env) { + // Timestamp at which this IP will next be allowed to send a message. Start in the distant + // past, i.e. the IP can send a message now. + this.nextAllowedTime = 0; + } + + // Our protocol is: POST when the IP performs an action, or GET to simply read the current limit. + // Either way, the result is the number of seconds to wait before allowing the IP to perform its + // next action. + async fetch(request) { + return await handleErrors(request, async () => { + let now = Date.now() / 1000; + + this.nextAllowedTime = Math.max(now, this.nextAllowedTime); + + if (request.method == "POST") { + // POST request means the user performed an action. + // We allow one action per 5 seconds. + this.nextAllowedTime += 5; + } + + // Return the number of seconds that the client needs to wait. + // + // We provide a "grace" period of 20 seconds, meaning that the client can make 4-5 requests + // in a quick burst before they start being limited. + let cooldown = Math.max(0, this.nextAllowedTime - now - 20); + return new Response(cooldown); + }) + } +} + +// RateLimiterClient implements rate limiting logic on the caller's side. +class RateLimiterClient { + // The constructor takes two functions: + // * getLimiterStub() returns a new Durable Object stub for the RateLimiter object that manages + // the limit. This may be called multiple times as needed to reconnect, if the connection is + // lost. + // * reportError(err) is called when something goes wrong and the rate limiter is broken. It + // should probably disconnect the client, so that they can reconnect and start over. + constructor(getLimiterStub, reportError) { + this.getLimiterStub = getLimiterStub; + this.reportError = reportError; + + // Call the callback to get the initial stub. + this.limiter = getLimiterStub(); + + // When `inCooldown` is true, the rate limit is currently applied and checkLimit() will return + // false. + this.inCooldown = false; + } + + // Call checkLimit() when a message is received to decide if it should be blocked due to the + // rate limit. Returns `true` if the message should be accepted, `false` to reject. + checkLimit() { + if (this.inCooldown) { + return false; + } + this.inCooldown = true; + this.callLimiter(); + return true; + } + + // callLimiter() is an internal method which talks to the rate limiter. + async callLimiter() { + try { + let response; + try { + // Currently, fetch() needs a valid URL even though it's not actually going to the + // internet. We may loosen this in the future to accept an arbitrary string. But for now, + // we have to provide a dummy URL that will be ignored at the other end anyway. + response = await this.limiter.fetch("https://dummy-url", {method: "POST"}); + } catch (err) { + // `fetch()` threw an exception. This is probably because the limiter has been + // disconnected. Stubs implement E-order semantics, meaning that calls to the same stub + // are delivered to the remote object in order, until the stub becomes disconnected, after + // which point all further calls fail. This guarantee makes a lot of complex interaction + // patterns easier, but it means we must be prepared for the occasional disconnect, as + // networks are inherently unreliable. + // + // Anyway, get a new limiter and try again. If it fails again, something else is probably + // wrong. + this.limiter = this.getLimiterStub(); + response = await this.limiter.fetch("https://dummy-url", {method: "POST"}); + } + + // The response indicates how long we want to pause before accepting more requests. + let cooldown = +(await response.text()); + await new Promise(resolve => setTimeout(resolve, cooldown * 1000)); + + // Done waiting. + this.inCooldown = false; + } catch (err) { + this.reportError(err); + } + } +} From 1526f2b02378145c6b4497d8592af3afd96718ca Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 21:49:33 -0300 Subject: [PATCH 02/72] . --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1ff00093..8c06290c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: Deploy on: push: branches: - - main + - master jobs: deploy: From 63f7b7a747c013ad648270e718be1e25cf1bfd20 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 21:52:14 -0300 Subject: [PATCH 03/72] . --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c06290c..3469cd53 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,8 +61,8 @@ jobs: curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME \ -X PUT \ -F "metadata=@metadata.json;type=application/json" \ - -F "script=@playground/serverless/worker.mjs;type=application/javascript+module" \ - -F "html=@playground/serverless/index.html;type=application/octet-stream" > /dev/null + -F "script=@playground\\serverless\\worker.mjs;type=application/javascript+module" \ + -F "html=@playground\\serverless\\index.html;type=application/octet-stream" > /dev/null } # upload_bootstrap_script is a temporary hack to work around a chicken-and-egg problem: in order @@ -74,12 +74,12 @@ jobs: # # This is obviously dumb and we (Cloudflare) will come up with something better soon. upload_bootstrap_script() { - echo '{"main_module": "playground/serverless/worker.mjs"}' > bootstrap-metadata.json + echo '{"main_module": "playground\\serverless\\worker.mjs"}' > bootstrap-metadata.json curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME \ -X PUT \ -F "metadata=@bootstrap-metadata.json;type=application/json" \ - -F "script=@playground/serverless/worker.mjs;type=application/javascript+module" \ - -F "html=@playground/serverless/index.html;type=application/octet-stream" > /dev/null + -F "script=@playground\\serverless\\worker.mjs;type=application/javascript+module" \ + -F "html=@playground\\serverless\\index.html;type=application/octet-stream" > /dev/null rm bootstrap-metadata.json } @@ -115,7 +115,7 @@ jobs: cat > metadata.json << __EOF__ { - "main_module": "playground/serverless/worker.mjs", + "main_module": "playground\\serverless\\worker.mjs", "bindings": [ { "type": "durable_object_namespace", From ac0e5fb2052fe3545f1e4fc64ef631da9398e410 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 21:56:53 -0300 Subject: [PATCH 04/72] . --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3469cd53..9c4d7826 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -74,7 +74,7 @@ jobs: # # This is obviously dumb and we (Cloudflare) will come up with something better soon. upload_bootstrap_script() { - echo '{"main_module": "playground\\serverless\\worker.mjs"}' > bootstrap-metadata.json + echo '{"main_module": "worker.mjs"}' > bootstrap-metadata.json curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME \ -X PUT \ -F "metadata=@bootstrap-metadata.json;type=application/json" \ @@ -115,7 +115,7 @@ jobs: cat > metadata.json << __EOF__ { - "main_module": "playground\\serverless\\worker.mjs", + "main_module": "worker.mjs", "bindings": [ { "type": "durable_object_namespace", From 5540db244be47c8242cc4de20d77b9730248d6df Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 21:58:29 -0300 Subject: [PATCH 05/72] . --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c4d7826..bbe97cc4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,8 +61,8 @@ jobs: curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME \ -X PUT \ -F "metadata=@metadata.json;type=application/json" \ - -F "script=@playground\\serverless\\worker.mjs;type=application/javascript+module" \ - -F "html=@playground\\serverless\\index.html;type=application/octet-stream" > /dev/null + -F "script=@playground/serverless/worker.mjs;type=application/javascript+module" \ + -F "html=@playground/serverless/index.html;type=application/octet-stream" > /dev/null } # upload_bootstrap_script is a temporary hack to work around a chicken-and-egg problem: in order @@ -78,8 +78,8 @@ jobs: curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME \ -X PUT \ -F "metadata=@bootstrap-metadata.json;type=application/json" \ - -F "script=@playground\\serverless\\worker.mjs;type=application/javascript+module" \ - -F "html=@playground\\serverless\\index.html;type=application/octet-stream" > /dev/null + -F "script=@playground/serverless/worker.mjs;type=application/javascript+module" \ + -F "html=@playground/serverless/index.html;type=application/octet-stream" > /dev/null rm bootstrap-metadata.json } From 63d0be1c79337f20e10aebb11eae2b231d550230 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 22:04:08 -0300 Subject: [PATCH 06/72] . --- .github/workflows/main.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bbe97cc4..5551fc21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,8 +61,8 @@ jobs: curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME \ -X PUT \ -F "metadata=@metadata.json;type=application/json" \ - -F "script=@playground/serverless/worker.mjs;type=application/javascript+module" \ - -F "html=@playground/serverless/index.html;type=application/octet-stream" > /dev/null + -F "script=@worker.mjs;type=application/javascript+module" \ + -F "html=@index.html;type=application/octet-stream" > /dev/null } # upload_bootstrap_script is a temporary hack to work around a chicken-and-egg problem: in order @@ -78,8 +78,8 @@ jobs: curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME \ -X PUT \ -F "metadata=@bootstrap-metadata.json;type=application/json" \ - -F "script=@playground/serverless/worker.mjs;type=application/javascript+module" \ - -F "html=@playground/serverless/index.html;type=application/octet-stream" > /dev/null + -F "script=@worker.mjs;type=application/javascript+module" \ + -F "html=@index.html;type=application/octet-stream" > /dev/null rm bootstrap-metadata.json } @@ -106,6 +106,8 @@ jobs: jq -r .id } + cd playground/serverless + if [ ! -e metadata.json ]; then # If metadata.json doesn't exist we assume this is first-time setup and we need to create the # namespaces. From bcf081aad4c4d1985799ea78fd6ba7c43393a6f9 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 22:06:31 -0300 Subject: [PATCH 07/72] Update main.yml --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5551fc21..d9cc6cb7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,7 @@ jobs: ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} AUTH_TOKEN: ${{ secrets.CF_API_TOKEN }} SCRIPT_NAME: "edge-game" + working-directory: playground/serverless run: | set -euo pipefail @@ -106,8 +107,6 @@ jobs: jq -r .id } - cd playground/serverless - if [ ! -e metadata.json ]; then # If metadata.json doesn't exist we assume this is first-time setup and we need to create the # namespaces. From 924fc06cd612db33c1e163b5f68b409e0dd18d6f Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 22:22:25 -0300 Subject: [PATCH 08/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index a6cc690d..114ab635 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -54,7 +54,7 @@ // the HTML content as an `ArrayBuffer`. So, we can serve our app's static asset without relying on // any separate storage. (However, the space available for assets served this way is very limited; // larger sites should continue to use Workers KV to serve assets.) -import HTML from "chat.html"; +import HTML from "index.html"; // `handleErrors()` is a little utility function that can wrap an HTTP request handler in a // try/catch and return errors to the client. You probably wouldn't want to use this in production From 63a9ba064af73bb5b94ed66438cb85846533566f Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 22:41:30 -0300 Subject: [PATCH 09/72] . --- playground/serverless/chat.html | 480 ++++++++++++++++++++++++++ playground/serverless/index.html | 557 +++++-------------------------- 2 files changed, 560 insertions(+), 477 deletions(-) create mode 100644 playground/serverless/chat.html diff --git a/playground/serverless/chat.html b/playground/serverless/chat.html new file mode 100644 index 00000000..50adb3c1 --- /dev/null +++ b/playground/serverless/chat.html @@ -0,0 +1,480 @@ + + + + + + + + + + + + + + + +
+ +

This chat runs entirely on the edge, powered by
+ Cloudflare Workers Durable Objects

+
+
+

Enter a public room:

+ +

OR

+ +
+
+
+
+
+
+ +
+ + + + + + + + \ No newline at end of file diff --git a/playground/serverless/index.html b/playground/serverless/index.html index 50adb3c1..3d2ae588 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -1,480 +1,83 @@ - - - - - - - - - - - - - -
- -

This chat runs entirely on the edge, powered by
- Cloudflare Workers Durable Objects

-
-
-

Enter a public room:

- -

OR

- -
-
-
-
-
-
- -
- - - - - - - + + + Meu Primeiro Jogo Multiplayer + + + + + + + + + \ No newline at end of file From 0013e4b108e757a49e04f9f6288c81b1db7283af Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 22:58:20 -0300 Subject: [PATCH 10/72] . --- playground/serverless/game.js | 154 +++++++++++++++++++ playground/serverless/keyboard-listener.js | 39 +++++ playground/serverless/render-screen.js | 28 ++++ playground/serverless/socket.io/socket.io.js | 9 ++ playground/serverless/worker.mjs | 4 +- 5 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 playground/serverless/game.js create mode 100644 playground/serverless/keyboard-listener.js create mode 100644 playground/serverless/render-screen.js create mode 100644 playground/serverless/socket.io/socket.io.js diff --git a/playground/serverless/game.js b/playground/serverless/game.js new file mode 100644 index 00000000..c9e0dbd2 --- /dev/null +++ b/playground/serverless/game.js @@ -0,0 +1,154 @@ +export default function createGame() { + const state = { + players: {}, + fruits: {}, + screen: { + width: 10, + height: 10 + } + } + + const observers = [] + + function start() { + const frequency = 2000 + + setInterval(addFruit, frequency) + } + + function subscribe(observerFunction) { + observers.push(observerFunction) + } + + function notifyAll(command) { + for (const observerFunction of observers) { + observerFunction(command) + } + } + + function setState(newState) { + Object.assign(state, newState) + } + + function addPlayer(command) { + const playerId = command.playerId + const playerX = 'playerX' in command ? command.playerX : Math.floor(Math.random() * state.screen.width) + const playerY = 'playerY' in command ? command.playerY : Math.floor(Math.random() * state.screen.height) + + state.players[playerId] = { + x: playerX, + y: playerY + } + + notifyAll({ + type: 'add-player', + playerId: playerId, + playerX: playerX, + playerY: playerY + }) + } + + function removePlayer(command) { + const playerId = command.playerId + + delete state.players[playerId] + + notifyAll({ + type: 'remove-player', + playerId: playerId + }) + } + + function addFruit(command) { + const fruitId = command ? command.fruitId : Math.floor(Math.random() * 10000000) + const fruitX = command ? command.fruitX : Math.floor(Math.random() * state.screen.width) + const fruitY = command ? command.fruitY : Math.floor(Math.random() * state.screen.height) + + state.fruits[fruitId] = { + x: fruitX, + y: fruitY + } + + notifyAll({ + type: 'add-fruit', + fruitId: fruitId, + fruitX: fruitX, + fruitY: fruitY + }) + } + + function removeFruit(command) { + const fruitId = command.fruitId + + delete state.fruits[fruitId] + + notifyAll({ + type: 'remove-fruit', + fruitId: fruitId, + }) + } + + function movePlayer(command) { + notifyAll(command) + + const acceptedMoves = { + ArrowUp(player) { + if (player.y - 1 >= 0) { + player.y = player.y - 1 + } + }, + ArrowRight(player) { + if (player.x + 1 < state.screen.width) { + player.x = player.x + 1 + } + }, + ArrowDown(player) { + if (player.y + 1 < state.screen.height) { + player.y = player.y + 1 + } + }, + ArrowLeft(player) { + if (player.x - 1 >= 0) { + player.x = player.x - 1 + } + } + } + + const keyPressed = command.keyPressed + const playerId = command.playerId + const player = state.players[playerId] + const moveFunction = acceptedMoves[keyPressed] + + if (player && moveFunction) { + moveFunction(player) + checkForFruitCollision(playerId) + } + + } + + function checkForFruitCollision(playerId) { + const player = state.players[playerId] + + for (const fruitId in state.fruits) { + const fruit = state.fruits[fruitId] + console.log(`Checking ${playerId} and ${fruitId}`) + + if (player.x === fruit.x && player.y === fruit.y) { + console.log(`COLLISION between ${playerId} and ${fruitId}`) + removeFruit({ fruitId: fruitId }) + } + } + } + + return { + addPlayer, + removePlayer, + movePlayer, + addFruit, + removeFruit, + state, + setState, + subscribe, + start + } +} diff --git a/playground/serverless/keyboard-listener.js b/playground/serverless/keyboard-listener.js new file mode 100644 index 00000000..2abd2cc8 --- /dev/null +++ b/playground/serverless/keyboard-listener.js @@ -0,0 +1,39 @@ +export default function createKeyboardListener(document) { + const state = { + observers: [], + playerId: null + } + + function registerPlayerId(playerId) { + state.playerId = playerId + } + + function subscribe(observerFunction) { + state.observers.push(observerFunction) + } + + function notifyAll(command) { + for (const observerFunction of state.observers) { + observerFunction(command) + } + } + + document.addEventListener('keydown', handleKeydown) + + function handleKeydown(event) { + const keyPressed = event.key + + const command = { + type: 'move-player', + playerId: state.playerId, + keyPressed + } + + notifyAll(command) + } + + return { + subscribe, + registerPlayerId + } +} \ No newline at end of file diff --git a/playground/serverless/render-screen.js b/playground/serverless/render-screen.js new file mode 100644 index 00000000..75b50353 --- /dev/null +++ b/playground/serverless/render-screen.js @@ -0,0 +1,28 @@ +export default function renderScreen(screen, game, requestAnimationFrame, currentPlayerId) { + const context = screen.getContext('2d') + context.fillStyle = 'white' + context.clearRect(0, 0, 10, 10) + + for (const playerId in game.state.players) { + const player = game.state.players[playerId] + context.fillStyle = 'black' + context.fillRect(player.x, player.y, 1, 1) + } + + for (const fruitId in game.state.fruits) { + const fruit = game.state.fruits[fruitId] + context.fillStyle = 'green' + context.fillRect(fruit.x, fruit.y, 1, 1) + } + + const currentPlayer = game.state.players[currentPlayerId] + + if(currentPlayer) { + context.fillStyle = '#F0DB4F' + context.fillRect(currentPlayer.x, currentPlayer.y, 1, 1) + } + + requestAnimationFrame(() => { + renderScreen(screen, game, requestAnimationFrame, currentPlayerId) + }) +} diff --git a/playground/serverless/socket.io/socket.io.js b/playground/serverless/socket.io/socket.io.js new file mode 100644 index 00000000..270777b6 --- /dev/null +++ b/playground/serverless/socket.io/socket.io.js @@ -0,0 +1,9 @@ +/*! + * Socket.IO v2.3.0 + * (c) 2014-2019 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){function r(t,e){"object"==typeof t&&(e=t,t=void 0),e=e||{};var n,r=o(t),i=r.source,u=r.id,p=r.path,h=c[u]&&p in c[u].nsps,f=e.forceNew||e["force new connection"]||!1===e.multiplex||h;return f?(a("ignoring socket cache for %s",i),n=s(i,e)):(c[u]||(a("new io instance for %s",i),c[u]=s(i,e)),n=c[u]),r.query&&!e.query&&(e.query=r.query),n.socket(r.path,e)}var o=n(1),i=n(7),s=n(15),a=n(3)("socket.io-client");t.exports=e=r;var c=e.managers={};e.protocol=i.protocol,e.connect=r,e.Manager=n(15),e.Socket=n(39)},function(t,e,n){function r(t,e){var n=t;e=e||"undefined"!=typeof location&&location,null==t&&(t=e.protocol+"//"+e.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?e.protocol+t:e.host+t),/^(https?|wss?):\/\//.test(t)||(i("protocol-less url %s",t),t="undefined"!=typeof e?e.protocol+"//"+t:"https://"+t),i("parse %s",t),n=o(t)),n.port||(/^(http|ws)$/.test(n.protocol)?n.port="80":/^(http|ws)s$/.test(n.protocol)&&(n.port="443")),n.path=n.path||"/";var r=n.host.indexOf(":")!==-1,s=r?"["+n.host+"]":n.host;return n.id=n.protocol+"://"+s+":"+n.port,n.href=n.protocol+"://"+s+(e&&e.port===n.port?"":":"+n.port),n}var o=n(2),i=n(3)("socket.io-client:url");t.exports=r},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");o!=-1&&i!=-1&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s=n.exec(t||""),a={},c=14;c--;)a[r[c]]=s[c]||"";return o!=-1&&i!=-1&&(a.source=e,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a}},function(t,e,n){(function(r){"use strict";function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type&&!window.process.__nwjs)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(e){if(e[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+e[0]+(this.useColors?"%c ":" ")+"+"+t.exports.humanize(this.diff),this.useColors){var n="color: "+this.color;e.splice(1,0,n,"color: inherit");var r=0,o=0;e[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(r++,"%c"===t&&(o=r))}),e.splice(o,0,n)}}function s(){var t;return"object"===("undefined"==typeof console?"undefined":p(console))&&console.log&&(t=console).log.apply(t,arguments)}function a(t){try{t?e.storage.setItem("debug",t):e.storage.removeItem("debug")}catch(n){}}function c(){var t=void 0;try{t=e.storage.getItem("debug")}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function u(){try{return localStorage}catch(t){}}var p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage=u(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.exports=n(5)(e);var h=t.exports.formatters;h.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}}).call(e,n(4))},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(t){if(p===setTimeout)return setTimeout(t,0);if((p===n||!p)&&setTimeout)return p=setTimeout,setTimeout(t,0);try{return p(t,0)}catch(e){try{return p.call(null,t,0)}catch(e){return p.call(this,t,0)}}}function i(t){if(h===clearTimeout)return clearTimeout(t);if((h===r||!h)&&clearTimeout)return h=clearTimeout,clearTimeout(t);try{return h(t)}catch(e){try{return h.call(null,t)}catch(e){return h.call(this,t)}}}function s(){y&&l&&(y=!1,l.length?d=l.concat(d):m=-1,d.length&&a())}function a(){if(!y){var t=o(s);y=!0;for(var e=d.length;e;){for(l=d,d=[];++m1)for(var n=1;n100)){var e=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*h;case"weeks":case"week":case"w":return n*p;case"days":case"day":case"d":return n*u;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){var e=Math.abs(t);return e>=u?Math.round(t/u)+"d":e>=c?Math.round(t/c)+"h":e>=a?Math.round(t/a)+"m":e>=s?Math.round(t/s)+"s":t+"ms"}function o(t){var e=Math.abs(t);return e>=u?i(t,e,u,"day"):e>=c?i(t,e,c,"hour"):e>=a?i(t,e,a,"minute"):e>=s?i(t,e,s,"second"):t+" ms"}function i(t,e,n,r){var o=e>=1.5*n;return Math.round(t/n)+" "+r+(o?"s":"")}var s=1e3,a=60*s,c=60*a,u=24*c,p=7*u,h=365.25*u;t.exports=function(t,e){e=e||{};var i=typeof t;if("string"===i&&t.length>0)return n(t);if("number"===i&&isFinite(t))return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(){}function o(t){var n=""+t.type;if(e.BINARY_EVENT!==t.type&&e.BINARY_ACK!==t.type||(n+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(n+=t.nsp+","),null!=t.id&&(n+=t.id),null!=t.data){var r=i(t.data);if(r===!1)return g;n+=r}return f("encoded %j as %s",t,n),n}function i(t){try{return JSON.stringify(t)}catch(e){return!1}}function s(t,e){function n(t){var n=d.deconstructPacket(t),r=o(n.packet),i=n.buffers;i.unshift(r),e(i)}d.removeBlobs(t,n)}function a(){this.reconstructor=null}function c(t){var n=0,r={type:Number(t.charAt(0))};if(null==e.types[r.type])return h("unknown packet type "+r.type);if(e.BINARY_EVENT===r.type||e.BINARY_ACK===r.type){for(var o="";"-"!==t.charAt(++n)&&(o+=t.charAt(n),n!=t.length););if(o!=Number(o)||"-"!==t.charAt(n))throw new Error("Illegal attachments");r.attachments=Number(o)}if("/"===t.charAt(n+1))for(r.nsp="";++n;){var i=t.charAt(n);if(","===i)break;if(r.nsp+=i,n===t.length)break}else r.nsp="/";var s=t.charAt(n+1);if(""!==s&&Number(s)==s){for(r.id="";++n;){var i=t.charAt(n);if(null==i||Number(i)!=i){--n;break}if(r.id+=t.charAt(n),n===t.length)break}r.id=Number(r.id)}if(t.charAt(++n)){var a=u(t.substr(n)),c=a!==!1&&(r.type===e.ERROR||y(a));if(!c)return h("invalid payload");r.data=a}return f("decoded %s as %j",t,r),r}function u(t){try{return JSON.parse(t)}catch(e){return!1}}function p(t){this.reconPack=t,this.buffers=[]}function h(t){return{type:e.ERROR,data:"parser error: "+t}}var f=n(8)("socket.io-parser"),l=n(11),d=n(12),y=n(13),m=n(14);e.protocol=4,e.types=["CONNECT","DISCONNECT","EVENT","ACK","ERROR","BINARY_EVENT","BINARY_ACK"],e.CONNECT=0,e.DISCONNECT=1,e.EVENT=2,e.ACK=3,e.ERROR=4,e.BINARY_EVENT=5,e.BINARY_ACK=6,e.Encoder=r,e.Decoder=a;var g=e.ERROR+'"encode error"';r.prototype.encode=function(t,n){if(f("encoding packet %j",t),e.BINARY_EVENT===t.type||e.BINARY_ACK===t.type)s(t,n);else{var r=o(t);n([r])}},l(a.prototype),a.prototype.add=function(t){var n;if("string"==typeof t)n=c(t),e.BINARY_EVENT===n.type||e.BINARY_ACK===n.type?(this.reconstructor=new p(n),0===this.reconstructor.reconPack.attachments&&this.emit("decoded",n)):this.emit("decoded",n);else{if(!m(t)&&!t.base64)throw new Error("Unknown type: "+t);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");n=this.reconstructor.takeBinaryData(t),n&&(this.reconstructor=null,this.emit("decoded",n))}},a.prototype.destroy=function(){this.reconstructor&&this.reconstructor.finishedReconstruction()},p.prototype.takeBinaryData=function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=d.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null},p.prototype.finishedReconstruction=function(){this.reconPack=null,this.buffers=[]}},function(t,e,n){(function(r){"use strict";function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(t){var n=this.useColors;if(t[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+t[0]+(n?"%c ":" ")+"+"+e.humanize(this.diff),n){var r="color: "+this.color;t.splice(1,0,r,"color: inherit");var o=0,i=0;t[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(o++,"%c"===t&&(i=o))}),t.splice(i,0,r)}}function s(){return"object"===("undefined"==typeof console?"undefined":p(console))&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(t){try{null==t?e.storage.removeItem("debug"):e.storage.debug=t}catch(n){}}function c(){var t;try{t=e.storage.debug}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function u(){try{return window.localStorage}catch(t){}}var p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};e=t.exports=n(9),e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:u(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],e.formatters.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}},e.enable(c())}).call(e,n(4))},function(t,e,n){"use strict";function r(t){var n,r=0;for(n in t)r=(r<<5)-r+t.charCodeAt(n),r|=0;return e.colors[Math.abs(r)%e.colors.length]}function o(t){function n(){if(n.enabled){var t=n,r=+new Date,i=r-(o||r);t.diff=i,t.prev=o,t.curr=r,o=r;for(var s=new Array(arguments.length),a=0;a100)){var e=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*p;case"days":case"day":case"d":return n*u;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){return t>=u?Math.round(t/u)+"d":t>=c?Math.round(t/c)+"h":t>=a?Math.round(t/a)+"m":t>=s?Math.round(t/s)+"s":t+"ms"}function o(t){return i(t,u,"day")||i(t,c,"hour")||i(t,a,"minute")||i(t,s,"second")||t+" ms"}function i(t,e,n){if(!(t0)return n(t);if("number"===i&&isNaN(t)===!1)return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(t){if(t)return o(t)}function o(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments)}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,o=0;o0&&!this.encoding){var t=this.packetBuffer.shift();this.packet(t)}},r.prototype.cleanup=function(){p("cleanup");for(var t=this.subs.length,e=0;e=this._reconnectionAttempts)p("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();p("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(p("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(p("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(p("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},r.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},function(t,e,n){t.exports=n(17),t.exports.parser=n(24)},function(t,e,n){function r(t,e){return this instanceof r?(e=e||{},t&&"object"==typeof t&&(e=t,t=null),t?(t=p(t),e.hostname=t.host,e.secure="https"===t.protocol||"wss"===t.protocol,e.port=t.port,t.query&&(e.query=t.query)):e.host&&(e.hostname=p(e.host).host),this.secure=null!=e.secure?e.secure:"undefined"!=typeof location&&"https:"===location.protocol,e.hostname&&!e.port&&(e.port=this.secure?"443":"80"),this.agent=e.agent||!1,this.hostname=e.hostname||("undefined"!=typeof location?location.hostname:"localhost"),this.port=e.port||("undefined"!=typeof location&&location.port?location.port:this.secure?443:80),this.query=e.query||{},"string"==typeof this.query&&(this.query=h.decode(this.query)),this.upgrade=!1!==e.upgrade,this.path=(e.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!e.forceJSONP,this.jsonp=!1!==e.jsonp,this.forceBase64=!!e.forceBase64,this.enablesXDR=!!e.enablesXDR,this.withCredentials=!1!==e.withCredentials,this.timestampParam=e.timestampParam||"t",this.timestampRequests=e.timestampRequests,this.transports=e.transports||["polling","websocket"],this.transportOptions=e.transportOptions||{},this.readyState="",this.writeBuffer=[],this.prevBufferLen=0,this.policyPort=e.policyPort||843,this.rememberUpgrade=e.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=e.onlyBinaryUpgrades,this.perMessageDeflate=!1!==e.perMessageDeflate&&(e.perMessageDeflate||{}),!0===this.perMessageDeflate&&(this.perMessageDeflate={}),this.perMessageDeflate&&null==this.perMessageDeflate.threshold&&(this.perMessageDeflate.threshold=1024),this.pfx=e.pfx||null,this.key=e.key||null,this.passphrase=e.passphrase||null,this.cert=e.cert||null,this.ca=e.ca||null,this.ciphers=e.ciphers||null,this.rejectUnauthorized=void 0===e.rejectUnauthorized||e.rejectUnauthorized,this.forceNode=!!e.forceNode,this.isReactNative="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),("undefined"==typeof self||this.isReactNative)&&(e.extraHeaders&&Object.keys(e.extraHeaders).length>0&&(this.extraHeaders=e.extraHeaders),e.localAddress&&(this.localAddress=e.localAddress)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingIntervalTimer=null,this.pingTimeoutTimer=null,void this.open()):new r(t,e)}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=n(18),s=n(11),a=n(3)("engine.io-client:socket"),c=n(38),u=n(24),p=n(2),h=n(32);t.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=u.protocol,r.Socket=r,r.Transport=n(23),r.transports=n(18),r.parser=n(24),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=u.protocol,e.transport=t;var n=this.transportOptions[t]||{};this.id&&(e.sid=this.id);var r=new i[t]({query:e,socket:this,agent:n.agent||this.agent,hostname:n.hostname||this.hostname,port:n.port||this.port,secure:n.secure||this.secure,path:n.path||this.path,forceJSONP:n.forceJSONP||this.forceJSONP,jsonp:n.jsonp||this.jsonp,forceBase64:n.forceBase64||this.forceBase64,enablesXDR:n.enablesXDR||this.enablesXDR,withCredentials:n.withCredentials||this.withCredentials,timestampRequests:n.timestampRequests||this.timestampRequests,timestampParam:n.timestampParam||this.timestampParam,policyPort:n.policyPort||this.policyPort,pfx:n.pfx||this.pfx,key:n.key||this.key,passphrase:n.passphrase||this.passphrase,cert:n.cert||this.cert,ca:n.ca||this.ca,ciphers:n.ciphers||this.ciphers,rejectUnauthorized:n.rejectUnauthorized||this.rejectUnauthorized,perMessageDeflate:n.perMessageDeflate||this.perMessageDeflate,extraHeaders:n.extraHeaders||this.extraHeaders,forceNode:n.forceNode||this.forceNode,localAddress:n.localAddress||this.localAddress,requestTimeout:n.requestTimeout||this.requestTimeout,protocols:n.protocols||void 0,isReactNative:this.isReactNative});return r},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1)t="websocket";else{ +if(0===this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(f.onlyBinaryUpgrades){var e=!this.supportsBinary&&f.transport.supportsBinary;h=h||e}h||(a('probe transport "%s" opened',t),p.send([{type:"ping",data:"probe"}]),p.once("packet",function(e){if(!h)if("pong"===e.type&&"probe"===e.data){if(a('probe transport "%s" pong',t),f.upgrading=!0,f.emit("upgrading",p),!p)return;r.priorWebsocketSuccess="websocket"===p.name,a('pausing current transport "%s"',f.transport.name),f.transport.pause(function(){h||"closed"!==f.readyState&&(a("changing transport and sending upgrade packet"),u(),f.setTransport(p),p.send([{type:"upgrade"}]),f.emit("upgrade",p),p=null,f.upgrading=!1,f.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=p.name,f.emit("upgradeError",n)}}))}function n(){h||(h=!0,u(),p.close(),p=null)}function o(e){var r=new Error("probe error: "+e);r.transport=p.name,n(),a('probe transport "%s" failed because of error: %s',t,e),f.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){p&&t.name!==p.name&&(a('"%s" works - aborting "%s"',t.name,p.name),n())}function u(){p.removeListener("open",e),p.removeListener("error",o),p.removeListener("close",i),f.removeListener("close",s),f.removeListener("upgrading",c)}a('probing transport "%s"',t);var p=this.createTransport(t,{probe:1}),h=!1,f=this;r.priorWebsocketSuccess=!1,p.once("open",e),p.once("error",o),p.once("close",i),this.once("close",s),this.once("upgrading",c),p.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;t1?{type:b[o],data:t.substring(1)}:{type:b[o]}:C}var i=new Uint8Array(t),o=i[0],s=f(t,1);return w&&"blob"===n&&(s=new w([s])),{type:b[o],data:s}},e.decodeBase64Packet=function(t,e){var n=b[t.charAt(0)];if(!u)return{type:n,data:{base64:!0,data:t.substr(1)}};var r=u.decode(t.substr(1));return"blob"===e&&w&&(r=new w([r])),{type:n,data:r}},e.encodePayload=function(t,n,r){function o(t){return t.length+":"+t}function i(t,r){e.encodePacket(t,!!s&&n,!1,function(t){r(null,o(t))})}"function"==typeof n&&(r=n,n=null);var s=h(t);return n&&s?w&&!g?e.encodePayloadAsBlob(t,r):e.encodePayloadAsArrayBuffer(t,r):t.length?void c(t,i,function(t,e){return r(e.join(""))}):r("0:")},e.decodePayload=function(t,n,r){if("string"!=typeof t)return e.decodePayloadAsBinary(t,n,r);"function"==typeof n&&(r=n,n=null);var o;if(""===t)return r(C,0,1);for(var i,s,a="",c=0,u=t.length;c0;){for(var s=new Uint8Array(o),a=0===s[0],c="",u=1;255!==s[u];u++){if(c.length>310)return r(C,0,1);c+=s[u]}o=f(o,2+c.length),c=parseInt(c);var p=f(o,0,c);if(a)try{p=String.fromCharCode.apply(null,new Uint8Array(p))}catch(h){var l=new Uint8Array(p);p="";for(var u=0;ur&&(n=r),e>=r||e>=n||0===r)return new ArrayBuffer(0);for(var o=new Uint8Array(t),i=new Uint8Array(n-e),s=e,a=0;s=55296&&e<=56319&&o65535&&(e-=65536,o+=d(e>>>10&1023|55296),e=56320|1023&e),o+=d(e);return o}function o(t,e){if(t>=55296&&t<=57343){if(e)throw Error("Lone surrogate U+"+t.toString(16).toUpperCase()+" is not a scalar value");return!1}return!0}function i(t,e){return d(t>>e&63|128)}function s(t,e){if(0==(4294967168&t))return d(t);var n="";return 0==(4294965248&t)?n=d(t>>6&31|192):0==(4294901760&t)?(o(t,e)||(t=65533),n=d(t>>12&15|224),n+=i(t,6)):0==(4292870144&t)&&(n=d(t>>18&7|240),n+=i(t,12),n+=i(t,6)),n+=d(63&t|128)}function a(t,e){e=e||{};for(var r,o=!1!==e.strict,i=n(t),a=i.length,c=-1,u="";++c=f)throw Error("Invalid byte index");var t=255&h[l];if(l++,128==(192&t))return 63&t;throw Error("Invalid continuation byte")}function u(t){var e,n,r,i,s;if(l>f)throw Error("Invalid byte index");if(l==f)return!1;if(e=255&h[l],l++,0==(128&e))return e;if(192==(224&e)){if(n=c(),s=(31&e)<<6|n,s>=128)return s;throw Error("Invalid continuation byte")}if(224==(240&e)){if(n=c(),r=c(),s=(15&e)<<12|n<<6|r,s>=2048)return o(s,t)?s:65533;throw Error("Invalid continuation byte")}if(240==(248&e)&&(n=c(),r=c(),i=c(),s=(7&e)<<18|n<<12|r<<6|i,s>=65536&&s<=1114111))return s;throw Error("Invalid UTF-8 detected")}function p(t,e){e=e||{};var o=!1!==e.strict;h=n(t),f=h.length,l=0;for(var i,s=[];(i=u(o))!==!1;)s.push(i);return r(s)}/*! https://mths.be/utf8js v2.1.2 by @mathias */ +var h,f,l,d=String.fromCharCode;t.exports={version:"2.1.2",encode:a,decode:p}},function(t,e){!function(){"use strict";for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(256),r=0;r>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3===2?i=i.substring(0,i.length-1)+"=":o%3===1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(t){var e,r,o,i,s,a=.75*t.length,c=t.length,u=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var p=new ArrayBuffer(a),h=new Uint8Array(p);for(e=0;e>4,h[u++]=(15&o)<<4|i>>2,h[u++]=(3&i)<<6|63&s;return p}}()},function(t,e){function n(t){return t.map(function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var n=new Uint8Array(t.byteLength);n.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=n.buffer}return e}return t})}function r(t,e){e=e||{};var r=new i;return n(t).forEach(function(t){r.append(t)}),e.type?r.getBlob(e.type):r.getBlob()}function o(t,e){return new Blob(n(t),e||{})}var i="undefined"!=typeof i?i:"undefined"!=typeof WebKitBlobBuilder?WebKitBlobBuilder:"undefined"!=typeof MSBlobBuilder?MSBlobBuilder:"undefined"!=typeof MozBlobBuilder&&MozBlobBuilder,s=function(){try{var t=new Blob(["hi"]);return 2===t.size}catch(e){return!1}}(),a=s&&function(){try{var t=new Blob([new Uint8Array([1,2])]);return 2===t.size}catch(e){return!1}}(),c=i&&i.prototype.append&&i.prototype.getBlob;"undefined"!=typeof Blob&&(r.prototype=Blob.prototype,o.prototype=Blob.prototype),t.exports=function(){return s?a?Blob:o:c?r:void 0}()},function(t,e){e.encode=function(t){var e="";for(var n in t)t.hasOwnProperty(n)&&(e.length&&(e+="&"),e+=encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e},e.decode=function(t){for(var e={},n=t.split("&"),r=0,o=n.length;r0);return e}function r(t){var e=0;for(p=0;p';i=document.createElement(e)}catch(t){i=document.createElement("iframe"),i.name=o.iframeId,i.src="javascript:0"}i.id=o.iframeId,o.form.appendChild(i),o.iframe=i}var o=this;if(!this.form){var i,s=document.createElement("form"),a=document.createElement("textarea"),c=this.iframeId="eio_iframe_"+this.index;s.className="socketio",s.style.position="absolute",s.style.top="-1000px",s.style.left="-1000px",s.target=c,s.method="POST",s.setAttribute("accept-charset","utf-8"),a.name="d",s.appendChild(a),document.body.appendChild(s),this.form=s,this.area=a}this.form.action=this.uri(),r(),t=t.replace(p,"\\\n"),this.area.value=t.replace(u,"\\n");try{this.form.submit()}catch(h){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===o.iframe.readyState&&n()}:this.iframe.onload=n}}).call(e,function(){return this}())},function(t,e,n){function r(t){var e=t&&t.forceBase64;e&&(this.supportsBinary=!1),this.perMessageDeflate=t.perMessageDeflate,this.usingBrowserWebSocket=o&&!t.forceNode,this.protocols=t.protocols,this.usingBrowserWebSocket||(l=i),s.call(this,t)}var o,i,s=n(23),a=n(24),c=n(32),u=n(33),p=n(34),h=n(3)("engine.io-client:websocket");if("undefined"!=typeof WebSocket?o=WebSocket:"undefined"!=typeof self&&(o=self.WebSocket||self.MozWebSocket),"undefined"==typeof window)try{i=n(37)}catch(f){}var l=o||i;t.exports=r,u(r,s),r.prototype.name="websocket",r.prototype.supportsBinary=!0,r.prototype.doOpen=function(){if(this.check()){var t=this.uri(),e=this.protocols,n={agent:this.agent,perMessageDeflate:this.perMessageDeflate};n.pfx=this.pfx,n.key=this.key,n.passphrase=this.passphrase,n.cert=this.cert,n.ca=this.ca,n.ciphers=this.ciphers,n.rejectUnauthorized=this.rejectUnauthorized,this.extraHeaders&&(n.headers=this.extraHeaders),this.localAddress&&(n.localAddress=this.localAddress);try{this.ws=this.usingBrowserWebSocket&&!this.isReactNative?e?new l(t,e):new l(t):new l(t,e,n)}catch(r){return this.emit("error",r)}void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.supports&&this.ws.supports.binary?(this.supportsBinary=!0,this.ws.binaryType="nodebuffer"):this.ws.binaryType="arraybuffer",this.addEventListeners()}},r.prototype.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.onOpen()},this.ws.onclose=function(){t.onClose()},this.ws.onmessage=function(e){t.onData(e.data)},this.ws.onerror=function(e){t.onError("websocket error",e)}},r.prototype.write=function(t){function e(){n.emit("flush"),setTimeout(function(){n.writable=!0,n.emit("drain")},0)}var n=this;this.writable=!1;for(var r=t.length,o=0,i=r;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])}); +//# sourceMappingURL=socket.io.js.map \ No newline at end of file diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 114ab635..61d68796 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -55,6 +55,7 @@ // any separate storage. (However, the space available for assets served this way is very limited; // larger sites should continue to use Workers KV to serve assets.) import HTML from "index.html"; +import game from "game.js"; // `handleErrors()` is a little utility function that can wrap an HTTP request handler in a // try/catch and return errors to the client. You probably wouldn't want to use this in production @@ -105,7 +106,8 @@ export default { return handleApiRequest(path.slice(1), request, env); default: - return new Response("Not found", {status: 404}); + return new Response(game, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); + //return new Response("Not found", {status: 404}); } }); } From ed99aef882a8f86c92b39f5bb934a6d99a2b4dbf Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:00:43 -0300 Subject: [PATCH 11/72] . --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9cc6cb7..819ad223 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,6 +63,7 @@ jobs: -X PUT \ -F "metadata=@metadata.json;type=application/json" \ -F "script=@worker.mjs;type=application/javascript+module" \ + -F "script=@game.js;type=application/javascript" \ -F "html=@index.html;type=application/octet-stream" > /dev/null } @@ -80,6 +81,7 @@ jobs: -X PUT \ -F "metadata=@bootstrap-metadata.json;type=application/json" \ -F "script=@worker.mjs;type=application/javascript+module" \ + -F "script=@game.js;type=application/javascript" \ -F "html=@index.html;type=application/octet-stream" > /dev/null rm bootstrap-metadata.json } From f6479cdc228bf64dafd0a741089d801f9ae47214 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:02:38 -0300 Subject: [PATCH 12/72] . --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 819ad223..acda9268 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,7 +63,7 @@ jobs: -X PUT \ -F "metadata=@metadata.json;type=application/json" \ -F "script=@worker.mjs;type=application/javascript+module" \ - -F "script=@game.js;type=application/javascript" \ + -F "html=@game.js;type=application/octet-stream" \ -F "html=@index.html;type=application/octet-stream" > /dev/null } @@ -81,7 +81,7 @@ jobs: -X PUT \ -F "metadata=@bootstrap-metadata.json;type=application/json" \ -F "script=@worker.mjs;type=application/javascript+module" \ - -F "script=@game.js;type=application/javascript" \ + -F "html=@game.js;type=application/octet-stream" \ -F "html=@index.html;type=application/octet-stream" > /dev/null rm bootstrap-metadata.json } From abfc6fd07ea1ff6d5dcee6be2094abd12349cd38 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:15:40 -0300 Subject: [PATCH 13/72] . --- .github/workflows/main.yml | 6 + playground/serverless/chat.html | 480 -------------------------------- playground/serverless/game.js | 4 +- 3 files changed, 8 insertions(+), 482 deletions(-) delete mode 100644 playground/serverless/chat.html diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index acda9268..984c7af7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -64,6 +64,9 @@ jobs: -F "metadata=@metadata.json;type=application/json" \ -F "script=@worker.mjs;type=application/javascript+module" \ -F "html=@game.js;type=application/octet-stream" \ + -F "html=@keyboard-listener.js;type=application/octet-stream" \ + -F "html=@render-screen.js;type=application/octet-stream" \ + -F "html=@socket.io\\socket.io.js;type=application/octet-stream" \ -F "html=@index.html;type=application/octet-stream" > /dev/null } @@ -82,6 +85,9 @@ jobs: -F "metadata=@bootstrap-metadata.json;type=application/json" \ -F "script=@worker.mjs;type=application/javascript+module" \ -F "html=@game.js;type=application/octet-stream" \ + -F "html=@keyboard-listener.js;type=application/octet-stream" \ + -F "html=@render-screen.js;type=application/octet-stream" \ + -F "html=@socket.io\\socket.io.js;type=application/octet-stream" \ -F "html=@index.html;type=application/octet-stream" > /dev/null rm bootstrap-metadata.json } diff --git a/playground/serverless/chat.html b/playground/serverless/chat.html deleted file mode 100644 index 50adb3c1..00000000 --- a/playground/serverless/chat.html +++ /dev/null @@ -1,480 +0,0 @@ - - - - - - - - - - - - - - - -
- -

This chat runs entirely on the edge, powered by
- Cloudflare Workers Durable Objects

-
-
-

Enter a public room:

- -

OR

- -
-
-
-
-
-
- -
- - - - - - - - \ No newline at end of file diff --git a/playground/serverless/game.js b/playground/serverless/game.js index c9e0dbd2..536e0283 100644 --- a/playground/serverless/game.js +++ b/playground/serverless/game.js @@ -1,4 +1,4 @@ -export default function createGame() { +function createGame() { const state = { players: {}, fruits: {}, @@ -151,4 +151,4 @@ export default function createGame() { subscribe, start } -} +}(); From ac4582d66ca119c1125a2c9e1ea2b7a9e9a5d981 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:17:14 -0300 Subject: [PATCH 14/72] . --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 984c7af7..32a2dca1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -66,7 +66,7 @@ jobs: -F "html=@game.js;type=application/octet-stream" \ -F "html=@keyboard-listener.js;type=application/octet-stream" \ -F "html=@render-screen.js;type=application/octet-stream" \ - -F "html=@socket.io\\socket.io.js;type=application/octet-stream" \ + -F "html=@socket.io/socket.io.js;type=application/octet-stream" \ -F "html=@index.html;type=application/octet-stream" > /dev/null } @@ -87,7 +87,7 @@ jobs: -F "html=@game.js;type=application/octet-stream" \ -F "html=@keyboard-listener.js;type=application/octet-stream" \ -F "html=@render-screen.js;type=application/octet-stream" \ - -F "html=@socket.io\\socket.io.js;type=application/octet-stream" \ + -F "html=@socket.io/socket.io.js;type=application/octet-stream" \ -F "html=@index.html;type=application/octet-stream" > /dev/null rm bootstrap-metadata.json } From b50fb917d8aa7c4bc7716f837e9f594512def75c Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:24:03 -0300 Subject: [PATCH 15/72] . --- playground/serverless/game.js | 2 +- playground/serverless/worker.mjs | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/playground/serverless/game.js b/playground/serverless/game.js index 536e0283..a5684749 100644 --- a/playground/serverless/game.js +++ b/playground/serverless/game.js @@ -151,4 +151,4 @@ function createGame() { subscribe, start } -}(); +}; diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 61d68796..49495b0c 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -56,6 +56,9 @@ // larger sites should continue to use Workers KV to serve assets.) import HTML from "index.html"; import game from "game.js"; +import keyboard_listener from "keyboard-listener.js"; +import render_screen from "render-screen.js"; +import socket_io from "socket.io/socket.io.js"; // `handleErrors()` is a little utility function that can wrap an HTTP request handler in a // try/catch and return errors to the client. You probably wouldn't want to use this in production @@ -105,9 +108,21 @@ export default { // This is a request for `/api/...`, call the API handler. return handleApiRequest(path.slice(1), request, env); - default: + import keyboard_listener from "keyboard-listener.js"; + import render_screen from "render-screen.js"; + import socket_io from "socket.io/socket.io.js"; + + case "game.js": return new Response(game, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); - //return new Response("Not found", {status: 404}); + case "keyboard-listener": + return new Response(keyboard_listener, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); + case "render-screen": + return new Response(render_screen, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); + case "socket.io": + return new Response(socket_io, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); + + default: + return new Response("Not found", {status: 404}); } }); } From 2e7d01d2de7088b1fca7041d395531ad5b5a4409 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:25:04 -0300 Subject: [PATCH 16/72] . --- playground/serverless/worker.mjs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 49495b0c..f5860fc5 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -108,10 +108,6 @@ export default { // This is a request for `/api/...`, call the API handler. return handleApiRequest(path.slice(1), request, env); - import keyboard_listener from "keyboard-listener.js"; - import render_screen from "render-screen.js"; - import socket_io from "socket.io/socket.io.js"; - case "game.js": return new Response(game, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); case "keyboard-listener": From 0a31ad8a71044597cdc44d771609e16757ad129e Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:26:07 -0300 Subject: [PATCH 17/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index f5860fc5..5a0fbfc1 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -58,7 +58,7 @@ import HTML from "index.html"; import game from "game.js"; import keyboard_listener from "keyboard-listener.js"; import render_screen from "render-screen.js"; -import socket_io from "socket.io/socket.io.js"; +import socket_io from "socket.io.js"; // `handleErrors()` is a little utility function that can wrap an HTTP request handler in a // try/catch and return errors to the client. You probably wouldn't want to use this in production From 6965a1554852ef06450ae7d7e1046070831a01f8 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:28:05 -0300 Subject: [PATCH 18/72] . --- playground/serverless/worker.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 5a0fbfc1..d5dbcf8e 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -110,9 +110,9 @@ export default { case "game.js": return new Response(game, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); - case "keyboard-listener": + case "keyboard-listener.js": return new Response(keyboard_listener, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); - case "render-screen": + case "render-screen.js": return new Response(render_screen, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); case "socket.io": return new Response(socket_io, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); From 5d6dd4df9d040f81163ae317884651a0a752aa22 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:30:48 -0300 Subject: [PATCH 19/72] . --- playground/serverless/game.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/serverless/game.js b/playground/serverless/game.js index a5684749..c9e0dbd2 100644 --- a/playground/serverless/game.js +++ b/playground/serverless/game.js @@ -1,4 +1,4 @@ -function createGame() { +export default function createGame() { const state = { players: {}, fruits: {}, @@ -151,4 +151,4 @@ function createGame() { subscribe, start } -}; +} From 1ebe016193fe15e60e8ebea96478553148a2e050 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:36:11 -0300 Subject: [PATCH 20/72] . --- playground/serverless/worker.mjs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index d5dbcf8e..433cd6b7 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -107,17 +107,11 @@ export default { case "api": // This is a request for `/api/...`, call the API handler. return handleApiRequest(path.slice(1), request, env); - - case "game.js": - return new Response(game, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); - case "keyboard-listener.js": - return new Response(keyboard_listener, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); - case "render-screen.js": - return new Response(render_screen, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); - case "socket.io": - return new Response(socket_io, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); - default: + if (url.pathname == '/game.js') return new Response(game, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); + if (url.pathname == '/keyboard-listener.js') return new Response(keyboard_listener, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); + if (url.pathname == '/render-screen.js') return new Response(render_screen, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); + if (url.pathname == '/socket.io/socket.io.js') return new Response(socket_io, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); return new Response("Not found", {status: 404}); } }); From fa51eff02040d2dde15892923398db706947fb1f Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:49:52 -0300 Subject: [PATCH 21/72] . --- playground/serverless/index.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/playground/serverless/index.html b/playground/serverless/index.html index 3d2ae588..d883ad2b 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -1,4 +1,3 @@ - @@ -28,7 +27,7 @@ const keyboardListener = createKeyboardListener(document) - const socket = io() + const socket = io({path: '/api/room/game/websocket'}) socket.on('connect', () => { const playerId = socket.id From c6d8034dada6203b7c764fb9d1db74cee7ac241d Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:55:13 -0300 Subject: [PATCH 22/72] . --- playground/serverless/worker.mjs | 44 +++++++++++++++----------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 433cd6b7..59fe61a9 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -235,34 +235,30 @@ export class ChatRoom { async fetch(request) { return await handleErrors(request, async () => { let url = new URL(request.url); + if (url.pathname.indexOf('/websocket') != -1) { + // The request is to `/api/room//websocket`. A client is trying to establish a new + // WebSocket session. + if (request.headers.get("Upgrade") != "websocket") { + return new Response("expected websocket", {status: 400}); + } - switch (url.pathname) { - case "/websocket": { - // The request is to `/api/room//websocket`. A client is trying to establish a new - // WebSocket session. - if (request.headers.get("Upgrade") != "websocket") { - return new Response("expected websocket", {status: 400}); - } - - // Get the client's IP address for use with the rate limiter. - let ip = request.headers.get("CF-Connecting-IP"); - - // To accept the WebSocket request, we create a WebSocketPair (which is like a socketpair, - // i.e. two WebSockets that talk to each other), we return one end of the pair in the - // response, and we operate on the other end. Note that this API is not part of the - // Fetch API standard; unfortunately, the Fetch API / Service Workers specs do not define - // any way to act as a WebSocket server today. - let pair = new WebSocketPair(); + // Get the client's IP address for use with the rate limiter. + let ip = request.headers.get("CF-Connecting-IP"); - // We're going to take pair[1] as our end, and return pair[0] to the client. - await this.handleSession(pair[1], ip); + // To accept the WebSocket request, we create a WebSocketPair (which is like a socketpair, + // i.e. two WebSockets that talk to each other), we return one end of the pair in the + // response, and we operate on the other end. Note that this API is not part of the + // Fetch API standard; unfortunately, the Fetch API / Service Workers specs do not define + // any way to act as a WebSocket server today. + let pair = new WebSocketPair(); - // Now we return the other end of the pair to the client. - return new Response(null, { status: 101, webSocket: pair[0] }); - } + // We're going to take pair[1] as our end, and return pair[0] to the client. + await this.handleSession(pair[1], ip); - default: - return new Response("Not found", {status: 404}); + // Now we return the other end of the pair to the client. + return new Response(null, { status: 101, webSocket: pair[0] }); + } else { + return new Response("Not found", {status: 404}); } }); } From 1af302dbb54514d5027336f2ed28f1e3e66fb830 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Mon, 30 Nov 2020 23:58:31 -0300 Subject: [PATCH 23/72] . --- playground/serverless/worker.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 59fe61a9..7c62d946 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -312,12 +312,14 @@ export class ChatRoom { } // Check if the user is over their rate limit and reject the message if so. + /* if (!limiter.checkLimit()) { webSocket.send(JSON.stringify({ error: "Your IP is being rate-limited, please try again later." })); return; } + */ // I guess we'll use JSON. let data = JSON.parse(msg.data); From 6a1b1767e9d026e4c5cf65d1bb83a9f6902798cc Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 00:21:41 -0300 Subject: [PATCH 24/72] . --- playground/serverless/index.html | 32 ++++++++++++++++++++++++++++++-- playground/serverless/worker.mjs | 5 +++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/playground/serverless/index.html b/playground/serverless/index.html index d883ad2b..8db9cd1f 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -25,10 +25,37 @@ const game = createGame() const keyboardListener = createKeyboardListener(document) - - const socket = io({path: '/api/room/game/websocket'}) + let currentWebSocket = null; + let hostname = window.location.host; + if (hostname == "localhost") { + hostname = "edge.bohr.cloud"; + } + + //const socket = io({path: '/api/room/game/websocket'}) + const ws = new WebSocket("wss://" + hostname + "/api/room/game/websocket"); + + ws.addEventListener("open", event => { + console.log('open'); + currentWebSocket = ws; + //ws.send(JSON.stringify({name: username})); + }); + + ws.addEventListener("message", event => { + let data = JSON.parse(event.data); + console.log(data); + }); + + ws.addEventListener("close", event => { + console.log("WebSocket closed, reconnecting:", event.code, event.reason); + //rejoin(); + }); + ws.addEventListener("error", event => { + console.log("WebSocket error, reconnecting:", event); + //rejoin(); + }); + /* socket.on('connect', () => { const playerId = socket.id console.log(`Player connected on Client with id: ${playerId}`) @@ -77,6 +104,7 @@ console.log(`Receiving ${command.type} -> ${command.fruitId}`) game.removeFruit(command) }) + */ \ No newline at end of file diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 7c62d946..524b868e 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -270,10 +270,12 @@ export class ChatRoom { webSocket.accept(); // Set up our rate limiter client. + /* let limiterId = this.env.limiters.idFromName(ip); let limiter = new RateLimiterClient( () => this.env.limiters.get(limiterId), err => webSocket.close(1011, err.stack)); + */ // Create our session and add it to the sessions list. // We don't send any messages to the client until it has sent us the initial user info @@ -299,6 +301,9 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; + + webSocket.send(JSON.stringify({emit: "setup", data:{}})); + webSocket.addEventListener("message", async msg => { try { if (session.quit) { From f9c4a5d0d0b91ceb07874faa69c98f824ff79c62 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 00:26:43 -0300 Subject: [PATCH 25/72] . --- playground/serverless/index.html | 1 + playground/serverless/worker.mjs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/playground/serverless/index.html b/playground/serverless/index.html index 8db9cd1f..7bcd91fd 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -39,6 +39,7 @@ console.log('open'); currentWebSocket = ws; //ws.send(JSON.stringify({name: username})); + //currentWebSocket.send(JSON.stringify({message: chatInput.value})); }); ws.addEventListener("message", event => { diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 524b868e..8fbbdd81 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -304,6 +304,10 @@ export class ChatRoom { webSocket.send(JSON.stringify({emit: "setup", data:{}})); + setInterval(function() { + webSocket.send(JSON.stringify({emit: "interval", data:{r:Math.random()}})); + }, 1000); + webSocket.addEventListener("message", async msg => { try { if (session.quit) { From a6626ab97166d2b178832034fb7e07d41336d753 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 00:42:06 -0300 Subject: [PATCH 26/72] . --- playground/serverless/worker.mjs | 33 +++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 8fbbdd81..449d6961 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -302,11 +302,26 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; - webSocket.send(JSON.stringify({emit: "setup", data:{}})); + const game = createGame(); + game.start(); + game.subscribe((command) => { + //console.log(`> Emitting ${command.type}`) + //sockets.emit(command.type, command) + }); + + const playerId = Math.random(); //socket.id + + game.addPlayer({ playerId: playerId }); + + //socket.emit('setup', game.state) + webSocket.send(JSON.stringify({emit: "setup", data:game.state})); + + /* setInterval(function() { webSocket.send(JSON.stringify({emit: "interval", data:{r:Math.random()}})); }, 1000); + */ webSocket.addEventListener("message", async msg => { try { @@ -330,6 +345,14 @@ export class ChatRoom { } */ + /* + socket.on('move-player', (command) => { + command.playerId = playerId + command.type = 'move-player' + game.movePlayer(command) + }); + */ + // I guess we'll use JSON. let data = JSON.parse(msg.data); @@ -396,6 +419,14 @@ export class ChatRoom { // On "close" and "error" events, remove the WebSocket from the sessions list and broadcast // a quit message. let closeOrErrorHandler = evt => { + + /* + socket.on('disconnect', () => { + game.removePlayer({ playerId: playerId }) + console.log(`> Player disconnected: ${playerId}`) + }) + */ + session.quit = true; this.sessions = this.sessions.filter(member => member !== session); if (session.name) { From aae6fbd83fb39d3510871dd952f3fe14b9117824 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 00:45:15 -0300 Subject: [PATCH 27/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 449d6961..54df437b 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -302,7 +302,7 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; - const game = createGame(); + const game = game(); game.start(); game.subscribe((command) => { From 256746653be5a12d41b1214fbb97e49c73b7666b Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 00:52:49 -0300 Subject: [PATCH 28/72] . --- playground/serverless/worker.mjs | 157 ++++++++++++++++++++++++++++++- 1 file changed, 156 insertions(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 54df437b..5030b05e 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -302,7 +302,7 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; - const game = game(); + const game = createGame(); game.start(); game.subscribe((command) => { @@ -581,3 +581,158 @@ class RateLimiterClient { } } } + +function createGame() { + const state = { + players: {}, + fruits: {}, + screen: { + width: 10, + height: 10 + } + } + + const observers = [] + + function start() { + const frequency = 2000 + + setInterval(addFruit, frequency) + } + + function subscribe(observerFunction) { + observers.push(observerFunction) + } + + function notifyAll(command) { + for (const observerFunction of observers) { + observerFunction(command) + } + } + + function setState(newState) { + Object.assign(state, newState) + } + + function addPlayer(command) { + const playerId = command.playerId + const playerX = 'playerX' in command ? command.playerX : Math.floor(Math.random() * state.screen.width) + const playerY = 'playerY' in command ? command.playerY : Math.floor(Math.random() * state.screen.height) + + state.players[playerId] = { + x: playerX, + y: playerY + } + + notifyAll({ + type: 'add-player', + playerId: playerId, + playerX: playerX, + playerY: playerY + }) + } + + function removePlayer(command) { + const playerId = command.playerId + + delete state.players[playerId] + + notifyAll({ + type: 'remove-player', + playerId: playerId + }) + } + + function addFruit(command) { + const fruitId = command ? command.fruitId : Math.floor(Math.random() * 10000000) + const fruitX = command ? command.fruitX : Math.floor(Math.random() * state.screen.width) + const fruitY = command ? command.fruitY : Math.floor(Math.random() * state.screen.height) + + state.fruits[fruitId] = { + x: fruitX, + y: fruitY + } + + notifyAll({ + type: 'add-fruit', + fruitId: fruitId, + fruitX: fruitX, + fruitY: fruitY + }) + } + + function removeFruit(command) { + const fruitId = command.fruitId + + delete state.fruits[fruitId] + + notifyAll({ + type: 'remove-fruit', + fruitId: fruitId, + }) + } + + function movePlayer(command) { + notifyAll(command) + + const acceptedMoves = { + ArrowUp(player) { + if (player.y - 1 >= 0) { + player.y = player.y - 1 + } + }, + ArrowRight(player) { + if (player.x + 1 < state.screen.width) { + player.x = player.x + 1 + } + }, + ArrowDown(player) { + if (player.y + 1 < state.screen.height) { + player.y = player.y + 1 + } + }, + ArrowLeft(player) { + if (player.x - 1 >= 0) { + player.x = player.x - 1 + } + } + } + + const keyPressed = command.keyPressed + const playerId = command.playerId + const player = state.players[playerId] + const moveFunction = acceptedMoves[keyPressed] + + if (player && moveFunction) { + moveFunction(player) + checkForFruitCollision(playerId) + } + + } + + function checkForFruitCollision(playerId) { + const player = state.players[playerId] + + for (const fruitId in state.fruits) { + const fruit = state.fruits[fruitId] + console.log(`Checking ${playerId} and ${fruitId}`) + + if (player.x === fruit.x && player.y === fruit.y) { + console.log(`COLLISION between ${playerId} and ${fruitId}`) + removeFruit({ fruitId: fruitId }) + } + } + } + + return { + addPlayer, + removePlayer, + movePlayer, + addFruit, + removeFruit, + state, + setState, + subscribe, + start + } +} \ No newline at end of file From cfb7c7eaad1613a0dc98463e036a8b49af8d0dbc Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 01:00:27 -0300 Subject: [PATCH 29/72] . --- playground/serverless/worker.mjs | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 5030b05e..043bb41d 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -308,6 +308,7 @@ export class ChatRoom { game.subscribe((command) => { //console.log(`> Emitting ${command.type}`) //sockets.emit(command.type, command) + webSocket.send(JSON.stringify({emit: command.type, data:command})); }); const playerId = Math.random(); //socket.id @@ -317,12 +318,6 @@ export class ChatRoom { //socket.emit('setup', game.state) webSocket.send(JSON.stringify({emit: "setup", data:game.state})); - /* - setInterval(function() { - webSocket.send(JSON.stringify({emit: "interval", data:{r:Math.random()}})); - }, 1000); - */ - webSocket.addEventListener("message", async msg => { try { if (session.quit) { @@ -345,17 +340,15 @@ export class ChatRoom { } */ - /* - socket.on('move-player', (command) => { - command.playerId = playerId - command.type = 'move-player' - game.movePlayer(command) - }); - */ - // I guess we'll use JSON. let data = JSON.parse(msg.data); + if (data.emit == 'move-player') { + data.data.playerId = playerId; + data.data.type = 'move-player'; + game.movePlayer(data.data); + } + if (!receivedUserInfo) { // The first message the client sends is the user info message with their name. Save it // into their session object. @@ -420,12 +413,7 @@ export class ChatRoom { // a quit message. let closeOrErrorHandler = evt => { - /* - socket.on('disconnect', () => { - game.removePlayer({ playerId: playerId }) - console.log(`> Player disconnected: ${playerId}`) - }) - */ + game.removePlayer({ playerId: playerId }); session.quit = true; this.sessions = this.sessions.filter(member => member !== session); From 9ce1d29f8b404bbe691050deac2e2203488b41e2 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 01:30:24 -0300 Subject: [PATCH 30/72] . --- playground/serverless/game.js | 14 +++++++++++++- playground/serverless/worker.mjs | 10 +++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/playground/serverless/game.js b/playground/serverless/game.js index c9e0dbd2..e1a2c093 100644 --- a/playground/serverless/game.js +++ b/playground/serverless/game.js @@ -1,4 +1,4 @@ -export default function createGame() { +export default function createGame(storage) { const state = { players: {}, fruits: {}, @@ -8,6 +8,10 @@ export default function createGame() { } } + function saveState() { + storage.put("GAME", JSON.stringify(state)); + } + const observers = [] function start() { @@ -40,6 +44,8 @@ export default function createGame() { y: playerY } + saveState(); + notifyAll({ type: 'add-player', playerId: playerId, @@ -53,6 +59,8 @@ export default function createGame() { delete state.players[playerId] + saveState(); + notifyAll({ type: 'remove-player', playerId: playerId @@ -69,6 +77,8 @@ export default function createGame() { y: fruitY } + saveState(); + notifyAll({ type: 'add-fruit', fruitId: fruitId, @@ -82,6 +92,8 @@ export default function createGame() { delete state.fruits[fruitId] + saveState(); + notifyAll({ type: 'remove-fruit', fruitId: fruitId, diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 043bb41d..55764955 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -302,7 +302,11 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; - const game = createGame(); + const game = createGame(this.storage); + const game_data = this.storage.get("GAME"); + if (game_data != null) { + game.setState(JSON.parse(game_data)); + } game.start(); game.subscribe((command) => { @@ -400,8 +404,8 @@ export class ChatRoom { this.broadcast(dataStr); // Save message. - let key = new Date(data.timestamp).toISOString(); - await this.storage.put(key, dataStr); + //let key = new Date(data.timestamp).toISOString(); + //await this.storage.put(key, dataStr); } catch (err) { // Report any exceptions directly back to the client. As with our handleErrors() this // probably isn't what you'd want to do in production, but it's convenient when testing. From 3b38794887c2fb55498f061b065d564825c194f1 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 01:35:09 -0300 Subject: [PATCH 31/72] . --- playground/serverless/worker.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 55764955..d46a9a96 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -305,6 +305,7 @@ export class ChatRoom { const game = createGame(this.storage); const game_data = this.storage.get("GAME"); if (game_data != null) { + webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); game.setState(JSON.parse(game_data)); } game.start(); From cec738dfdc071de80b41fe9b3386828d1c7a87eb Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 01:36:55 -0300 Subject: [PATCH 32/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index d46a9a96..c56fc939 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -306,7 +306,7 @@ export class ChatRoom { const game_data = this.storage.get("GAME"); if (game_data != null) { webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); - game.setState(JSON.parse(game_data)); + //game.setState(JSON.parse(game_data)); } game.start(); From de6525f89d6dbf109eb8b3cacaef904a95ccde7d Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 01:43:12 -0300 Subject: [PATCH 33/72] . --- playground/serverless/game.js | 2 +- playground/serverless/worker.mjs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/playground/serverless/game.js b/playground/serverless/game.js index e1a2c093..62f3ef18 100644 --- a/playground/serverless/game.js +++ b/playground/serverless/game.js @@ -9,7 +9,7 @@ export default function createGame(storage) { } function saveState() { - storage.put("GAME", JSON.stringify(state)); + await storage.put("GAME", JSON.stringify(state)); } const observers = [] diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index c56fc939..f67443a9 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -303,10 +303,10 @@ export class ChatRoom { let receivedUserInfo = false; const game = createGame(this.storage); - const game_data = this.storage.get("GAME"); - if (game_data != null) { - webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); - //game.setState(JSON.parse(game_data)); + const game_data = await this.storage.get("GAME"); + webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); + if (!((Object.keys(obj).length === 0) && (obj.constructor === Object))) { + game.setState(game_data); } game.start(); From d61c411fee1c2cb8b4a6448f192be224efbd84fa Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 01:47:16 -0300 Subject: [PATCH 34/72] . --- playground/serverless/game.js | 14 +------------- playground/serverless/worker.mjs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/playground/serverless/game.js b/playground/serverless/game.js index 62f3ef18..c9e0dbd2 100644 --- a/playground/serverless/game.js +++ b/playground/serverless/game.js @@ -1,4 +1,4 @@ -export default function createGame(storage) { +export default function createGame() { const state = { players: {}, fruits: {}, @@ -8,10 +8,6 @@ export default function createGame(storage) { } } - function saveState() { - await storage.put("GAME", JSON.stringify(state)); - } - const observers = [] function start() { @@ -44,8 +40,6 @@ export default function createGame(storage) { y: playerY } - saveState(); - notifyAll({ type: 'add-player', playerId: playerId, @@ -59,8 +53,6 @@ export default function createGame(storage) { delete state.players[playerId] - saveState(); - notifyAll({ type: 'remove-player', playerId: playerId @@ -77,8 +69,6 @@ export default function createGame(storage) { y: fruitY } - saveState(); - notifyAll({ type: 'add-fruit', fruitId: fruitId, @@ -92,8 +82,6 @@ export default function createGame(storage) { delete state.fruits[fruitId] - saveState(); - notifyAll({ type: 'remove-fruit', fruitId: fruitId, diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index f67443a9..053e79e3 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -575,7 +575,7 @@ class RateLimiterClient { } } -function createGame() { +function createGame(storage) { const state = { players: {}, fruits: {}, @@ -585,6 +585,10 @@ function createGame() { } } + function saveState() { + await storage.put("GAME", JSON.stringify(state)); + } + const observers = [] function start() { @@ -617,6 +621,8 @@ function createGame() { y: playerY } + saveState(); + notifyAll({ type: 'add-player', playerId: playerId, @@ -630,6 +636,8 @@ function createGame() { delete state.players[playerId] + saveState(); + notifyAll({ type: 'remove-player', playerId: playerId @@ -646,6 +654,8 @@ function createGame() { y: fruitY } + saveState(); + notifyAll({ type: 'add-fruit', fruitId: fruitId, @@ -659,6 +669,8 @@ function createGame() { delete state.fruits[fruitId] + saveState(); + notifyAll({ type: 'remove-fruit', fruitId: fruitId, From d4827c65d0b191f4e0ca3f235a5286ffc08263d4 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 01:48:52 -0300 Subject: [PATCH 35/72] . --- playground/serverless/worker.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 053e79e3..2324bc81 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -575,7 +575,7 @@ class RateLimiterClient { } } -function createGame(storage) { +function createGame(_storage) { const state = { players: {}, fruits: {}, @@ -586,7 +586,7 @@ function createGame(storage) { } function saveState() { - await storage.put("GAME", JSON.stringify(state)); + await _storage.put("GAME", JSON.stringify(state)); } const observers = [] From b1fc80813cd5d72e740b7854f749eecb8cd36630 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 01:55:07 -0300 Subject: [PATCH 36/72] . --- playground/serverless/worker.mjs | 336 +++++++++++++++---------------- 1 file changed, 168 insertions(+), 168 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 2324bc81..9813e650 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -228,6 +228,173 @@ export class ChatRoom { this.lastTimestamp = 0; } + createGame() { + const state = { + players: {}, + fruits: {}, + screen: { + width: 10, + height: 10 + } + } + + function saveState() { + await this.storage.put("GAME", JSON.stringify(state)); + } + + const observers = [] + + function start() { + const frequency = 2000 + + setInterval(addFruit, frequency) + } + + function subscribe(observerFunction) { + observers.push(observerFunction) + } + + function notifyAll(command) { + for (const observerFunction of observers) { + observerFunction(command) + } + } + + function setState(newState) { + Object.assign(state, newState) + } + + function addPlayer(command) { + const playerId = command.playerId + const playerX = 'playerX' in command ? command.playerX : Math.floor(Math.random() * state.screen.width) + const playerY = 'playerY' in command ? command.playerY : Math.floor(Math.random() * state.screen.height) + + state.players[playerId] = { + x: playerX, + y: playerY + } + + saveState(); + + notifyAll({ + type: 'add-player', + playerId: playerId, + playerX: playerX, + playerY: playerY + }) + } + + function removePlayer(command) { + const playerId = command.playerId + + delete state.players[playerId] + + saveState(); + + notifyAll({ + type: 'remove-player', + playerId: playerId + }) + } + + function addFruit(command) { + const fruitId = command ? command.fruitId : Math.floor(Math.random() * 10000000) + const fruitX = command ? command.fruitX : Math.floor(Math.random() * state.screen.width) + const fruitY = command ? command.fruitY : Math.floor(Math.random() * state.screen.height) + + state.fruits[fruitId] = { + x: fruitX, + y: fruitY + } + + saveState(); + + notifyAll({ + type: 'add-fruit', + fruitId: fruitId, + fruitX: fruitX, + fruitY: fruitY + }) + } + + function removeFruit(command) { + const fruitId = command.fruitId + + delete state.fruits[fruitId] + + saveState(); + + notifyAll({ + type: 'remove-fruit', + fruitId: fruitId, + }) + } + + function movePlayer(command) { + notifyAll(command) + + const acceptedMoves = { + ArrowUp(player) { + if (player.y - 1 >= 0) { + player.y = player.y - 1 + } + }, + ArrowRight(player) { + if (player.x + 1 < state.screen.width) { + player.x = player.x + 1 + } + }, + ArrowDown(player) { + if (player.y + 1 < state.screen.height) { + player.y = player.y + 1 + } + }, + ArrowLeft(player) { + if (player.x - 1 >= 0) { + player.x = player.x - 1 + } + } + } + + const keyPressed = command.keyPressed + const playerId = command.playerId + const player = state.players[playerId] + const moveFunction = acceptedMoves[keyPressed] + + if (player && moveFunction) { + moveFunction(player) + checkForFruitCollision(playerId) + } + + } + + function checkForFruitCollision(playerId) { + const player = state.players[playerId] + + for (const fruitId in state.fruits) { + const fruit = state.fruits[fruitId] + console.log(`Checking ${playerId} and ${fruitId}`) + + if (player.x === fruit.x && player.y === fruit.y) { + console.log(`COLLISION between ${playerId} and ${fruitId}`) + removeFruit({ fruitId: fruitId }) + } + } + } + + return { + addPlayer, + removePlayer, + movePlayer, + addFruit, + removeFruit, + state, + setState, + subscribe, + start + } + } + // The system will call fetch() whenever an HTTP request is sent to this Object. Such requests // can only be sent from other Worker code, such as the code above; these requests don't come // directly from the internet. In the future, we will support other formats than HTTP for these @@ -302,7 +469,7 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; - const game = createGame(this.storage); + const game = createGame(); const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); if (!((Object.keys(obj).length === 0) && (obj.constructor === Object))) { @@ -573,171 +740,4 @@ class RateLimiterClient { this.reportError(err); } } -} - -function createGame(_storage) { - const state = { - players: {}, - fruits: {}, - screen: { - width: 10, - height: 10 - } - } - - function saveState() { - await _storage.put("GAME", JSON.stringify(state)); - } - - const observers = [] - - function start() { - const frequency = 2000 - - setInterval(addFruit, frequency) - } - - function subscribe(observerFunction) { - observers.push(observerFunction) - } - - function notifyAll(command) { - for (const observerFunction of observers) { - observerFunction(command) - } - } - - function setState(newState) { - Object.assign(state, newState) - } - - function addPlayer(command) { - const playerId = command.playerId - const playerX = 'playerX' in command ? command.playerX : Math.floor(Math.random() * state.screen.width) - const playerY = 'playerY' in command ? command.playerY : Math.floor(Math.random() * state.screen.height) - - state.players[playerId] = { - x: playerX, - y: playerY - } - - saveState(); - - notifyAll({ - type: 'add-player', - playerId: playerId, - playerX: playerX, - playerY: playerY - }) - } - - function removePlayer(command) { - const playerId = command.playerId - - delete state.players[playerId] - - saveState(); - - notifyAll({ - type: 'remove-player', - playerId: playerId - }) - } - - function addFruit(command) { - const fruitId = command ? command.fruitId : Math.floor(Math.random() * 10000000) - const fruitX = command ? command.fruitX : Math.floor(Math.random() * state.screen.width) - const fruitY = command ? command.fruitY : Math.floor(Math.random() * state.screen.height) - - state.fruits[fruitId] = { - x: fruitX, - y: fruitY - } - - saveState(); - - notifyAll({ - type: 'add-fruit', - fruitId: fruitId, - fruitX: fruitX, - fruitY: fruitY - }) - } - - function removeFruit(command) { - const fruitId = command.fruitId - - delete state.fruits[fruitId] - - saveState(); - - notifyAll({ - type: 'remove-fruit', - fruitId: fruitId, - }) - } - - function movePlayer(command) { - notifyAll(command) - - const acceptedMoves = { - ArrowUp(player) { - if (player.y - 1 >= 0) { - player.y = player.y - 1 - } - }, - ArrowRight(player) { - if (player.x + 1 < state.screen.width) { - player.x = player.x + 1 - } - }, - ArrowDown(player) { - if (player.y + 1 < state.screen.height) { - player.y = player.y + 1 - } - }, - ArrowLeft(player) { - if (player.x - 1 >= 0) { - player.x = player.x - 1 - } - } - } - - const keyPressed = command.keyPressed - const playerId = command.playerId - const player = state.players[playerId] - const moveFunction = acceptedMoves[keyPressed] - - if (player && moveFunction) { - moveFunction(player) - checkForFruitCollision(playerId) - } - - } - - function checkForFruitCollision(playerId) { - const player = state.players[playerId] - - for (const fruitId in state.fruits) { - const fruit = state.fruits[fruitId] - console.log(`Checking ${playerId} and ${fruitId}`) - - if (player.x === fruit.x && player.y === fruit.y) { - console.log(`COLLISION between ${playerId} and ${fruitId}`) - removeFruit({ fruitId: fruitId }) - } - } - } - - return { - addPlayer, - removePlayer, - movePlayer, - addFruit, - removeFruit, - state, - setState, - subscribe, - start - } } \ No newline at end of file From 232496563e348025f95b1ffb7076404af6995d0c Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:01:49 -0300 Subject: [PATCH 37/72] . --- playground/serverless/worker.mjs | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 9813e650..f2af70a8 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -238,7 +238,7 @@ export class ChatRoom { } } - function saveState() { + async function saveState() { await this.storage.put("GAME", JSON.stringify(state)); } @@ -264,7 +264,7 @@ export class ChatRoom { Object.assign(state, newState) } - function addPlayer(command) { + async function addPlayer(command) { const playerId = command.playerId const playerX = 'playerX' in command ? command.playerX : Math.floor(Math.random() * state.screen.width) const playerY = 'playerY' in command ? command.playerY : Math.floor(Math.random() * state.screen.height) @@ -274,30 +274,30 @@ export class ChatRoom { y: playerY } - saveState(); - notifyAll({ type: 'add-player', playerId: playerId, playerX: playerX, playerY: playerY }) + + await saveState(); } - function removePlayer(command) { + async function removePlayer(command) { const playerId = command.playerId delete state.players[playerId] - saveState(); - notifyAll({ type: 'remove-player', playerId: playerId }) + + await saveState(); } - function addFruit(command) { + async function addFruit(command) { const fruitId = command ? command.fruitId : Math.floor(Math.random() * 10000000) const fruitX = command ? command.fruitX : Math.floor(Math.random() * state.screen.width) const fruitY = command ? command.fruitY : Math.floor(Math.random() * state.screen.height) @@ -307,30 +307,30 @@ export class ChatRoom { y: fruitY } - saveState(); - notifyAll({ type: 'add-fruit', fruitId: fruitId, fruitX: fruitX, fruitY: fruitY }) + + await saveState(); } - function removeFruit(command) { + async function removeFruit(command) { const fruitId = command.fruitId delete state.fruits[fruitId] - saveState(); - notifyAll({ type: 'remove-fruit', fruitId: fruitId, }) + + await saveState(); } - function movePlayer(command) { + async function movePlayer(command) { notifyAll(command) const acceptedMoves = { @@ -363,12 +363,12 @@ export class ChatRoom { if (player && moveFunction) { moveFunction(player) - checkForFruitCollision(playerId) + await checkForFruitCollision(playerId) } } - function checkForFruitCollision(playerId) { + async function checkForFruitCollision(playerId) { const player = state.players[playerId] for (const fruitId in state.fruits) { @@ -377,7 +377,7 @@ export class ChatRoom { if (player.x === fruit.x && player.y === fruit.y) { console.log(`COLLISION between ${playerId} and ${fruitId}`) - removeFruit({ fruitId: fruitId }) + await removeFruit({ fruitId: fruitId }) } } } @@ -485,7 +485,7 @@ export class ChatRoom { const playerId = Math.random(); //socket.id - game.addPlayer({ playerId: playerId }); + await game.addPlayer({ playerId: playerId }); //socket.emit('setup', game.state) webSocket.send(JSON.stringify({emit: "setup", data:game.state})); @@ -518,7 +518,7 @@ export class ChatRoom { if (data.emit == 'move-player') { data.data.playerId = playerId; data.data.type = 'move-player'; - game.movePlayer(data.data); + await game.movePlayer(data.data); } if (!receivedUserInfo) { @@ -585,7 +585,7 @@ export class ChatRoom { // a quit message. let closeOrErrorHandler = evt => { - game.removePlayer({ playerId: playerId }); + await game.removePlayer({ playerId: playerId }); session.quit = true; this.sessions = this.sessions.filter(member => member !== session); From f9d614fd57cded256307cc9db7777bc173132532 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:05:34 -0300 Subject: [PATCH 38/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index f2af70a8..0b393a23 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -583,7 +583,7 @@ export class ChatRoom { // On "close" and "error" events, remove the WebSocket from the sessions list and broadcast // a quit message. - let closeOrErrorHandler = evt => { + let closeOrErrorHandler = async evt => { await game.removePlayer({ playerId: playerId }); From 7f54f609e1d094124e0782f872102f1b7109a5e7 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:08:11 -0300 Subject: [PATCH 39/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 0b393a23..ca066d54 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -469,7 +469,7 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; - const game = createGame(); + const game = this.createGame(); const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); if (!((Object.keys(obj).length === 0) && (obj.constructor === Object))) { From e3c6b741c15864e80e24eff02288c0e753437ae2 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:09:48 -0300 Subject: [PATCH 40/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index ca066d54..ff3c9cce 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -472,7 +472,7 @@ export class ChatRoom { const game = this.createGame(); const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); - if (!((Object.keys(obj).length === 0) && (obj.constructor === Object))) { + if (!((Object.keys(game_data).length === 0) && (game_data.constructor === Object))) { game.setState(game_data); } game.start(); From 354ba4aa4a55922ef386e7801e0ff2572eecc433 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:11:15 -0300 Subject: [PATCH 41/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index ff3c9cce..5e1585bf 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -472,7 +472,7 @@ export class ChatRoom { const game = this.createGame(); const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); - if (!((Object.keys(game_data).length === 0) && (game_data.constructor === Object))) { + if (game_data != null) { game.setState(game_data); } game.start(); From a374d760ae6616d989a421542882a6a0352c9edf Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:12:54 -0300 Subject: [PATCH 42/72] . --- playground/serverless/worker.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 5e1585bf..2bee364b 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -228,7 +228,7 @@ export class ChatRoom { this.lastTimestamp = 0; } - createGame() { + createGame(storage) { const state = { players: {}, fruits: {}, @@ -239,7 +239,7 @@ export class ChatRoom { } async function saveState() { - await this.storage.put("GAME", JSON.stringify(state)); + await storage.put("GAME", JSON.stringify(state)); } const observers = [] @@ -469,7 +469,7 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; - const game = this.createGame(); + const game = this.createGame(this.storage); const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); if (game_data != null) { From 8c3e611c38540b708bd6e25c6aea219c92e2cc2a Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:19:01 -0300 Subject: [PATCH 43/72] . --- playground/serverless/worker.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 2bee364b..315bdda1 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -473,6 +473,7 @@ export class ChatRoom { const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); if (game_data != null) { + webSocket.send(JSON.stringify({emit: 'debug', data:JSON.parse(game_data)})); game.setState(game_data); } game.start(); From c179d931c41d0ae2c32470e593a6898ee225825d Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:21:36 -0300 Subject: [PATCH 44/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 315bdda1..e00d797c 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -239,7 +239,7 @@ export class ChatRoom { } async function saveState() { - await storage.put("GAME", JSON.stringify(state)); + await storage.put("GAME", state); } const observers = [] From accf1e243c720cbd021e0ea72df63eb0815159ef Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:22:42 -0300 Subject: [PATCH 45/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index e00d797c..37437038 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -474,7 +474,7 @@ export class ChatRoom { webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); if (game_data != null) { webSocket.send(JSON.stringify({emit: 'debug', data:JSON.parse(game_data)})); - game.setState(game_data); + //game.setState(game_data); } game.start(); From 005df3d0f82f20e1ff468283e03226a70007310c Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:24:13 -0300 Subject: [PATCH 46/72] . --- playground/serverless/worker.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 37437038..dcedb93f 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -473,7 +473,6 @@ export class ChatRoom { const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); if (game_data != null) { - webSocket.send(JSON.stringify({emit: 'debug', data:JSON.parse(game_data)})); //game.setState(game_data); } game.start(); From 4771871052ed16d80406991dcf6e09f9fcdf8ec9 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:27:20 -0300 Subject: [PATCH 47/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index dcedb93f..38e97b68 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -473,7 +473,7 @@ export class ChatRoom { const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); if (game_data != null) { - //game.setState(game_data); + game.setState(game_data); } game.start(); From 44e814aaba8ae115d2a210fffb30f839c7463001 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:32:17 -0300 Subject: [PATCH 48/72] . --- playground/serverless/worker.mjs | 33 +++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 38e97b68..07ba7126 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -298,23 +298,26 @@ export class ChatRoom { } async function addFruit(command) { - const fruitId = command ? command.fruitId : Math.floor(Math.random() * 10000000) - const fruitX = command ? command.fruitX : Math.floor(Math.random() * state.screen.width) - const fruitY = command ? command.fruitY : Math.floor(Math.random() * state.screen.height) + + if (state.fruits.length < 3) { + const fruitId = command ? command.fruitId : Math.floor(Math.random() * 10000000) + const fruitX = command ? command.fruitX : Math.floor(Math.random() * state.screen.width) + const fruitY = command ? command.fruitY : Math.floor(Math.random() * state.screen.height) + + state.fruits[fruitId] = { + x: fruitX, + y: fruitY + } + + notifyAll({ + type: 'add-fruit', + fruitId: fruitId, + fruitX: fruitX, + fruitY: fruitY + }) - state.fruits[fruitId] = { - x: fruitX, - y: fruitY + await saveState(); } - - notifyAll({ - type: 'add-fruit', - fruitId: fruitId, - fruitX: fruitX, - fruitY: fruitY - }) - - await saveState(); } async function removeFruit(command) { From 0040f4d0a3df2019ceb8a4d5df63b60ff4dba673 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:34:13 -0300 Subject: [PATCH 49/72] . --- playground/serverless/worker.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 07ba7126..6f560a77 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -299,7 +299,7 @@ export class ChatRoom { async function addFruit(command) { - if (state.fruits.length < 3) { + if (state.fruits.length < 1) { const fruitId = command ? command.fruitId : Math.floor(Math.random() * 10000000) const fruitX = command ? command.fruitX : Math.floor(Math.random() * state.screen.width) const fruitY = command ? command.fruitY : Math.floor(Math.random() * state.screen.height) @@ -476,7 +476,7 @@ export class ChatRoom { const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); if (game_data != null) { - game.setState(game_data); + //game.setState(game_data); } game.start(); From fe1b3ec1f13d4d76375966ad356162aed38e5319 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:37:31 -0300 Subject: [PATCH 50/72] . --- playground/serverless/worker.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 6f560a77..915bf720 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -299,7 +299,7 @@ export class ChatRoom { async function addFruit(command) { - if (state.fruits.length < 1) { + if (state.fruits.length < 3) { const fruitId = command ? command.fruitId : Math.floor(Math.random() * 10000000) const fruitX = command ? command.fruitX : Math.floor(Math.random() * state.screen.width) const fruitY = command ? command.fruitY : Math.floor(Math.random() * state.screen.height) @@ -472,6 +472,8 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; + await this.storage.delete("GAME"); + const game = this.createGame(this.storage); const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); From d2f314a58ba2454dd061cca1360c79a75d002b2d Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:38:40 -0300 Subject: [PATCH 51/72] . --- playground/serverless/worker.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 915bf720..a9cf270d 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -472,8 +472,6 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; - await this.storage.delete("GAME"); - const game = this.createGame(this.storage); const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); From 264b621d6e2d33d29c62545ff411b5ab3a16b582 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:46:11 -0300 Subject: [PATCH 52/72] . --- playground/serverless/worker.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index a9cf270d..f5dd2675 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -298,8 +298,7 @@ export class ChatRoom { } async function addFruit(command) { - - if (state.fruits.length < 3) { + if (Object.keys(state.fruits).length < 3) { const fruitId = command ? command.fruitId : Math.floor(Math.random() * 10000000) const fruitX = command ? command.fruitX : Math.floor(Math.random() * state.screen.width) const fruitY = command ? command.fruitY : Math.floor(Math.random() * state.screen.height) @@ -472,6 +471,8 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; + await this.storage.delete("GAME"); + const game = this.createGame(this.storage); const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); From 97ca58552522d261092aeaf53bd5ca99b2bcf2fd Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 02:50:17 -0300 Subject: [PATCH 53/72] . --- playground/serverless/worker.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index f5dd2675..a15b3864 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -471,13 +471,13 @@ export class ChatRoom { // Set event handlers to receive messages. let receivedUserInfo = false; - await this.storage.delete("GAME"); + //await this.storage.delete("GAME"); const game = this.createGame(this.storage); const game_data = await this.storage.get("GAME"); webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); if (game_data != null) { - //game.setState(game_data); + game.setState(game_data); } game.start(); From daa236371003b544234a4fa87dff2599ffe8364b Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 03:12:45 -0300 Subject: [PATCH 54/72] . --- playground/serverless/worker.mjs | 414 +------------------------------ 1 file changed, 7 insertions(+), 407 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index a15b3864..d797dac6 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -1,76 +1,14 @@ -// This is the Edge Chat Demo Worker, built using Durable Objects! - -// =============================== -// Introduction to Modules -// =============================== -// -// The first thing you might notice, if you are familiar with the Workers platform, is that this -// Worker is written differently from others you may have seen. It even has a different file -// extension. The `mjs` extension means this JavaScript is an ES Module, which, among other things, -// means it has imports and exports. Unlike other Workers, this code doesn't use -// `addEventListener("fetch", handler)` to register its main HTTP handler; instead, it _exports_ -// a handler, as we'll see below. -// -// This is a new way of writing Workers that we expect to introduce more broadly in the future. We -// like this syntax because it is *composable*: You can take two workers written this way and -// merge them into one worker, by importing the two Workers' exported handlers yourself, and then -// exporting a new handler that call into the other Workers as appropriate. -// -// This new syntax is required when using Durable Objects, because your Durable Objects are -// implemented by classes, and those classes need to be exported. The new syntax can be used for -// writing regular Workers (without Durable Objects) too, but for now, you must be in the Durable -// Objects beta to be able to use the new syntax, while we work out the quirks. -// -// To see the API for uploading module-based Workers, check out the publish.sh script. - -// =============================== -// Required Environment -// =============================== -// -// This worker, when deployed, must be configured with two environment bindings: -// * rooms: A Durable Object namespace binding mapped to the ChatRoom class. -// * limiters: A Durable Object namespace binding mapped to the RateLimiter class. -// -// Incidentally, in pre-modules Workers syntax, "bindings" (like KV bindings, secrets, etc.) -// appeared in your script as global variables, but in the new modules syntax, this is no longer -// the case. Instead, bindings are now delivered in an "environment object" when an event handler -// (or Durable Object class constructor) is called. Look for the variable `env` below. -// -// We made this change, again, for composability: The global scope is global, but if you want to -// call into existing code that has different environment requirements, then you need to be able -// to pass the environment as a parameter instead. -// -// Once again, see the publish.sh script to understand how the environment is configured. - -// ======================================================================================= -// The regular Worker part... -// -// This section of the code implements a normal Worker that receives HTTP requests from external -// clients. This part is stateless. - -// With the introduction of modules, we're experimenting with allowing text/data blobs to be -// uploaded and exposed as synthetic modules. We uploaded `chat.html` as a module of type -// `application/octet-stream`, i.e. just a byte blob. So when we import it as `HTML` here, we get -// the HTML content as an `ArrayBuffer`. So, we can serve our app's static asset without relying on -// any separate storage. (However, the space available for assets served this way is very limited; -// larger sites should continue to use Workers KV to serve assets.) import HTML from "index.html"; import game from "game.js"; import keyboard_listener from "keyboard-listener.js"; import render_screen from "render-screen.js"; import socket_io from "socket.io.js"; -// `handleErrors()` is a little utility function that can wrap an HTTP request handler in a -// try/catch and return errors to the client. You probably wouldn't want to use this in production -// code but it is convenient when debugging and iterating. async function handleErrors(request, func) { try { return await func(); } catch (err) { if (request.headers.get("Upgrade") == "websocket") { - // Annoyingly, if we return an HTTP error in response to a WebSocket request, Chrome devtools - // won't show us the response body! So... let's send a WebSocket response with an error - // frame instead. let pair = new WebSocketPair(); pair[1].accept(); pair[1].send(JSON.stringify({error: err.stack})); @@ -82,30 +20,14 @@ async function handleErrors(request, func) { } } -// In modules-syntax workers, we use `export default` to export our script's main event handlers. -// Here, we export one handler, `fetch`, for receiving HTTP requests. In pre-modules workers, the -// fetch handler was registered using `addEventHandler("fetch", event => { ... })`; this is just -// new syntax for essentially the same thing. -// -// `fetch` isn't the only handler. If your worker runs on a Cron schedule, it will receive calls -// to a handler named `scheduled`, which should be exported here in a similar way. We will be -// adding other handlers for other types of events over time. export default { async fetch(request, env) { return await handleErrors(request, async () => { - // We have received an HTTP request! Parse the URL and route the request. - let url = new URL(request.url); let path = url.pathname.slice(1).split('/'); - - if (!path[0]) { - // Serve our HTML at the root path. - return new Response(HTML, {headers: {"Content-Type": "text/html;charset=UTF-8"}}); - } - + if (!path[0]) return new Response(HTML, {headers: {"Content-Type": "text/html;charset=UTF-8"}}); switch (path[0]) { case "api": - // This is a request for `/api/...`, call the API handler. return handleApiRequest(path.slice(1), request, env); default: if (url.pathname == '/game.js') return new Response(game, {headers: {"Content-Type": "application/javascript; charset=utf-8"}}); @@ -117,83 +39,35 @@ export default { }); } } - - +/* async function handleApiRequest(path, request, env) { - // We've received at API request. Route the request based on the path. switch (path[0]) { case "room": { - // Request for `/api/room/...`. - if (!path[1]) { - // The request is for just "/api/room", with no ID. if (request.method == "POST") { - // POST to /api/room creates a private room. - // - // Incidentally, this code doesn't actually store anything. It just generates a valid - // unique ID for this namespace. Each durable object namespace has its own ID space, but - // IDs from one namespace are not valid for any other. - // - // The IDs returned by `newUniqueId()` are unguessable, so are a valid way to implement - // "anyone with the link can access" sharing. Additionally, IDs generated this way have - // a performance benefit over IDs generated from names: When a unique ID is generated, - // the system knows it is unique without having to communicate with the rest of the - // world -- i.e., there is no way that someone in the UK and someone in New Zealand - // could coincidentally create the same ID at the same time, because unique IDs are, - // well, unique! let id = env.rooms.newUniqueId(); return new Response(id.toString(), {headers: {"Access-Control-Allow-Origin": "*"}}); } else { - // If we wanted to support returning a list of public rooms, this might be a place to do - // it. The list of room names might be a good thing to store in KV, though a singleton - // Durable Object is also a possibility as long as the Cache API is used to cache reads. - // (A caching layer would be needed because a single Durable Object is single-threaded, - // so the amount of traffic it can handle is limited. Also, caching would improve latency - // for users who don't happen to be located close to the singleton.) - // - // For this demo, though, we're not implementing a public room list, mainly because - // inevitably some trolls would probably register a bunch of offensive room names. Sigh. return new Response("Method not allowed", {status: 405}); } } - // OK, the request is for `/api/room//...`. It's time to route to the Durable Object - // for the specific room. let name = path[1]; - - // Each Durable Object has a 256-bit unique ID. IDs can be derived from string names, or - // chosen randomly by the system. let id; if (name.match(/^[0-9a-f]{64}$/)) { - // The name is 64 hex digits, so let's assume it actually just encodes an ID. We use this - // for private rooms. `idFromString()` simply parses the text as a hex encoding of the raw - // ID (and verifies that this is a valid ID for this namespace). id = env.rooms.idFromString(name); } else if (name.length <= 32) { - // Treat as a string room name (limited to 32 characters). `idFromName()` consistently - // derives an ID from a string. id = env.rooms.idFromName(name); } else { return new Response("Name too long", {status: 404}); } - // Get the Durable Object stub for this room! The stub is a client object that can be used - // to send messages to the remote Durable Object instance. The stub is returned immediately; - // there is no need to await it. This is important because you would not want to wait for - // a network round trip before you could start sending requests. Since Durable Objects are - // created on-demand when the ID is first used, there's nothing to wait for anyway; we know - // an object will be available somewhere to receive our requests. let roomObject = env.rooms.get(id); - // Compute a new URL with `/api/room/` removed. We'll forward the rest of the path - // to the Durable Object. let newUrl = new URL(request.url); newUrl.pathname = "/" + path.slice(2).join("/"); - // Send the request to the object. The `fetch()` method of a Durable Object stub has the - // same signature as the global `fetch()` function, but the request is always sent to the - // object, regardless of the request's URL. return roomObject.fetch(newUrl, request); } @@ -201,31 +75,12 @@ async function handleApiRequest(path, request, env) { return new Response("Not found", {status: 404}); } } - -// ======================================================================================= -// The ChatRoom Durable Object Class - -// ChatRoom implements a Durable Object that coordinates an individual chat room. Participants -// connect to the room using WebSockets, and the room broadcasts messages from each participant -// to all others. +*/ export class ChatRoom { constructor(controller, env) { - // `controller.storage` provides access to our durable storage. It provides a simple KV - // get()/put() interface. this.storage = controller.storage; - - // `env` is our environment bindings (discussed earlier). this.env = env; - - // We will put the WebSocket objects for each client, along with some metadata, into - // `sessions`. this.sessions = []; - - // We keep track of the last-seen message's timestamp just so that we can assign monotonically - // increasing timestamps even if multiple messages arrive simultaneously (see below). There's - // no need to store this to disk since we assume if the object is destroyed and recreated, much - // more than a millisecond will have gone by. - this.lastTimestamp = 0; } createGame(storage) { @@ -246,14 +101,9 @@ export class ChatRoom { function start() { const frequency = 2000 - setInterval(addFruit, frequency) } - function subscribe(observerFunction) { - observers.push(observerFunction) - } - function notifyAll(command) { for (const observerFunction of observers) { observerFunction(command) @@ -392,15 +242,10 @@ export class ChatRoom { removeFruit, state, setState, - subscribe, start } } - // The system will call fetch() whenever an HTTP request is sent to this Object. Such requests - // can only be sent from other Worker code, such as the code above; these requests don't come - // directly from the internet. In the future, we will support other formats than HTTP for these - // communications, but we started with HTTP for its familiarity. async fetch(request) { return await handleErrors(request, async () => { let url = new URL(request.url); @@ -432,176 +277,44 @@ export class ChatRoom { }); } - // handleSession() implements our WebSocket-based chat protocol. async handleSession(webSocket, ip) { - // Accept our end of the WebSocket. This tells the runtime that we'll be terminating the - // WebSocket in JavaScript, not sending it elsewhere. - webSocket.accept(); - // Set up our rate limiter client. - /* - let limiterId = this.env.limiters.idFromName(ip); - let limiter = new RateLimiterClient( - () => this.env.limiters.get(limiterId), - err => webSocket.close(1011, err.stack)); - */ + webSocket.accept(); - // Create our session and add it to the sessions list. - // We don't send any messages to the client until it has sent us the initial user info - // message. Until then, we will queue messages in `session.blockedMessages`. - let session = {webSocket, blockedMessages: []}; + let session = {webSocket}; this.sessions.push(session); - // Queue "join" messages for all online users, to populate the client's roster. - this.sessions.forEach(otherSession => { - if (otherSession.name) { - session.blockedMessages.push(JSON.stringify({joined: otherSession.name})); - } - }); - - // Load the last 100 messages from the chat history stored on disk, and send them to the - // client. - let storage = await this.storage.list({reverse: true, limit: 100}); - let backlog = [...storage.values()]; - backlog.reverse(); - backlog.forEach(value => { - session.blockedMessages.push(value); - }); - - // Set event handlers to receive messages. - let receivedUserInfo = false; - - //await this.storage.delete("GAME"); - const game = this.createGame(this.storage); const game_data = await this.storage.get("GAME"); - webSocket.send(JSON.stringify({emit: 'debug', data:game_data})); - if (game_data != null) { - game.setState(game_data); - } + if (game_data != null) game.setState(game_data); game.start(); - game.subscribe((command) => { - //console.log(`> Emitting ${command.type}`) - //sockets.emit(command.type, command) - webSocket.send(JSON.stringify({emit: command.type, data:command})); - }); - - const playerId = Math.random(); //socket.id - + const playerId = Math.random(); await game.addPlayer({ playerId: playerId }); - //socket.emit('setup', game.state) webSocket.send(JSON.stringify({emit: "setup", data:game.state})); webSocket.addEventListener("message", async msg => { try { - if (session.quit) { - // Whoops, when trying to send to this WebSocket in the past, it threw an exception and - // we marked it broken. But somehow we got another message? I guess try sending a - // close(), which might throw, in which case we'll try to send an error, which will also - // throw, and whatever, at least we won't accept the message. (This probably can't - // actually happen. This is defensive coding.) - webSocket.close(1011, "WebSocket broken."); - return; - } - - // Check if the user is over their rate limit and reject the message if so. - /* - if (!limiter.checkLimit()) { - webSocket.send(JSON.stringify({ - error: "Your IP is being rate-limited, please try again later." - })); - return; - } - */ - - // I guess we'll use JSON. let data = JSON.parse(msg.data); - if (data.emit == 'move-player') { data.data.playerId = playerId; data.data.type = 'move-player'; await game.movePlayer(data.data); } - - if (!receivedUserInfo) { - // The first message the client sends is the user info message with their name. Save it - // into their session object. - session.name = "" + (data.name || "anonymous"); - - // Don't let people use ridiculously long names. (This is also enforced on the client, - // so if they get here they are not using the intended client.) - if (session.name.length > 32) { - webSocket.send(JSON.stringify({error: "Name too long."})); - webSocket.close(1009, "Name too long."); - return; - } - - // Deliver all the messages we queued up since the user connected. - session.blockedMessages.forEach(queued => { - webSocket.send(queued); - }); - delete session.blockedMessages; - - // Broadcast to all other connections that this user has joined. - this.broadcast({joined: session.name}); - - webSocket.send(JSON.stringify({ready: true})); - - // Note that we've now received the user info message. - receivedUserInfo = true; - - return; - } - - // Construct sanitized message for storage and broadcast. - data = { name: session.name, message: "" + data.message }; - - // Block people from sending overly long messages. This is also enforced on the client, - // so to trigger this the user must be bypassing the client code. - if (data.message.length > 256) { - webSocket.send(JSON.stringify({error: "Message too long."})); - return; - } - - // Add timestamp. Here's where this.lastTimestamp comes in -- if we receive a bunch of - // messages at the same time (or if the clock somehow goes backwards????), we'll assign - // them sequential timestamps, so at least the ordering is maintained. - data.timestamp = Math.max(Date.now(), this.lastTimestamp + 1); - this.lastTimestamp = data.timestamp; - - // Broadcast the message to all other WebSockets. - let dataStr = JSON.stringify(data); - this.broadcast(dataStr); - - // Save message. - //let key = new Date(data.timestamp).toISOString(); - //await this.storage.put(key, dataStr); } catch (err) { - // Report any exceptions directly back to the client. As with our handleErrors() this - // probably isn't what you'd want to do in production, but it's convenient when testing. webSocket.send(JSON.stringify({error: err.stack})); } }); - // On "close" and "error" events, remove the WebSocket from the sessions list and broadcast - // a quit message. let closeOrErrorHandler = async evt => { - await game.removePlayer({ playerId: playerId }); - - session.quit = true; this.sessions = this.sessions.filter(member => member !== session); - if (session.name) { - this.broadcast({quit: session.name}); - } }; webSocket.addEventListener("close", closeOrErrorHandler); webSocket.addEventListener("error", closeOrErrorHandler); } - // broadcast() broadcasts a message to all clients. broadcast(message) { // Apply JSON if we weren't given a string to start with. if (typeof message !== "string") { @@ -630,118 +343,5 @@ export class ChatRoom { } }); - quitters.forEach(quitter => { - if (quitter.name) { - this.broadcast({quit: quitter.name}); - } - }); } } - -// ======================================================================================= -// The RateLimiter Durable Object class. - -// RateLimiter implements a Durable Object that tracks the frequency of messages from a particular -// source and decides when messages should be dropped because the source is sending too many -// messages. -// -// We utilize this in ChatRoom, above, to apply a per-IP-address rate limit. These limits are -// global, i.e. they apply across all chat rooms, so if a user spams one chat room, they will find -// themselves rate limited in all other chat rooms simultaneously. -export class RateLimiter { - constructor(controller, env) { - // Timestamp at which this IP will next be allowed to send a message. Start in the distant - // past, i.e. the IP can send a message now. - this.nextAllowedTime = 0; - } - - // Our protocol is: POST when the IP performs an action, or GET to simply read the current limit. - // Either way, the result is the number of seconds to wait before allowing the IP to perform its - // next action. - async fetch(request) { - return await handleErrors(request, async () => { - let now = Date.now() / 1000; - - this.nextAllowedTime = Math.max(now, this.nextAllowedTime); - - if (request.method == "POST") { - // POST request means the user performed an action. - // We allow one action per 5 seconds. - this.nextAllowedTime += 5; - } - - // Return the number of seconds that the client needs to wait. - // - // We provide a "grace" period of 20 seconds, meaning that the client can make 4-5 requests - // in a quick burst before they start being limited. - let cooldown = Math.max(0, this.nextAllowedTime - now - 20); - return new Response(cooldown); - }) - } -} - -// RateLimiterClient implements rate limiting logic on the caller's side. -class RateLimiterClient { - // The constructor takes two functions: - // * getLimiterStub() returns a new Durable Object stub for the RateLimiter object that manages - // the limit. This may be called multiple times as needed to reconnect, if the connection is - // lost. - // * reportError(err) is called when something goes wrong and the rate limiter is broken. It - // should probably disconnect the client, so that they can reconnect and start over. - constructor(getLimiterStub, reportError) { - this.getLimiterStub = getLimiterStub; - this.reportError = reportError; - - // Call the callback to get the initial stub. - this.limiter = getLimiterStub(); - - // When `inCooldown` is true, the rate limit is currently applied and checkLimit() will return - // false. - this.inCooldown = false; - } - - // Call checkLimit() when a message is received to decide if it should be blocked due to the - // rate limit. Returns `true` if the message should be accepted, `false` to reject. - checkLimit() { - if (this.inCooldown) { - return false; - } - this.inCooldown = true; - this.callLimiter(); - return true; - } - - // callLimiter() is an internal method which talks to the rate limiter. - async callLimiter() { - try { - let response; - try { - // Currently, fetch() needs a valid URL even though it's not actually going to the - // internet. We may loosen this in the future to accept an arbitrary string. But for now, - // we have to provide a dummy URL that will be ignored at the other end anyway. - response = await this.limiter.fetch("https://dummy-url", {method: "POST"}); - } catch (err) { - // `fetch()` threw an exception. This is probably because the limiter has been - // disconnected. Stubs implement E-order semantics, meaning that calls to the same stub - // are delivered to the remote object in order, until the stub becomes disconnected, after - // which point all further calls fail. This guarantee makes a lot of complex interaction - // patterns easier, but it means we must be prepared for the occasional disconnect, as - // networks are inherently unreliable. - // - // Anyway, get a new limiter and try again. If it fails again, something else is probably - // wrong. - this.limiter = this.getLimiterStub(); - response = await this.limiter.fetch("https://dummy-url", {method: "POST"}); - } - - // The response indicates how long we want to pause before accepting more requests. - let cooldown = +(await response.text()); - await new Promise(resolve => setTimeout(resolve, cooldown * 1000)); - - // Done waiting. - this.inCooldown = false; - } catch (err) { - this.reportError(err); - } - } -} \ No newline at end of file From 08812a3c263e0b498114cebb315e6dd56d18b413 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 03:18:36 -0300 Subject: [PATCH 55/72] . --- playground/serverless/worker.mjs | 46 +++++++++++++------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index d797dac6..9c866dd0 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -97,17 +97,13 @@ export class ChatRoom { await storage.put("GAME", state); } - const observers = [] - function start() { const frequency = 2000 setInterval(addFruit, frequency) } function notifyAll(command) { - for (const observerFunction of observers) { - observerFunction(command) - } + broadcast(command); } function setState(newState) { @@ -296,6 +292,10 @@ export class ChatRoom { webSocket.addEventListener("message", async msg => { try { + if (session.quit) { + webSocket.close(1011, "WebSocket broken."); + return; + } let data = JSON.parse(msg.data); if (data.emit == 'move-player') { data.data.playerId = playerId; @@ -308,40 +308,30 @@ export class ChatRoom { }); let closeOrErrorHandler = async evt => { - await game.removePlayer({ playerId: playerId }); + session.quit = true; this.sessions = this.sessions.filter(member => member !== session); + await game.removePlayer({ playerId: playerId }); }; webSocket.addEventListener("close", closeOrErrorHandler); webSocket.addEventListener("error", closeOrErrorHandler); } broadcast(message) { - // Apply JSON if we weren't given a string to start with. - if (typeof message !== "string") { - message = JSON.stringify(message); - } - - // Iterate over all the sessions sending them messages. let quitters = []; this.sessions = this.sessions.filter(session => { - if (session.name) { - try { - session.webSocket.send(message); - return true; - } catch (err) { - // Whoops, this connection is dead. Remove it from the list and arrange to notify - // everyone below. - session.quit = true; - quitters.push(session); - return false; - } - } else { - // This session hasn't sent the initial user info message yet, so we're not sending them - // messages yet (no secret lurking!). Queue the message to be sent later. - session.blockedMessages.push(message); + try { + session.webSocket.send(message); return true; + } catch (err) { + session.quit = true; + quitters.push(session); + return false; + } + }); + quitters.forEach(quitter => { + if (quitter.name) { + this.broadcast({quit: quitter.name}); } }); - } } From 524398bac84776bbeb74200fd3337225b4d463a6 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 03:31:54 -0300 Subject: [PATCH 56/72] . --- .github/workflows/main.yml | 2 - playground/serverless/index.html | 149 +++++++++---------- playground/serverless/socket.io/socket.io.js | 9 -- playground/serverless/worker.mjs | 4 +- 4 files changed, 76 insertions(+), 88 deletions(-) delete mode 100644 playground/serverless/socket.io/socket.io.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 32a2dca1..8c599001 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -66,7 +66,6 @@ jobs: -F "html=@game.js;type=application/octet-stream" \ -F "html=@keyboard-listener.js;type=application/octet-stream" \ -F "html=@render-screen.js;type=application/octet-stream" \ - -F "html=@socket.io/socket.io.js;type=application/octet-stream" \ -F "html=@index.html;type=application/octet-stream" > /dev/null } @@ -87,7 +86,6 @@ jobs: -F "html=@game.js;type=application/octet-stream" \ -F "html=@keyboard-listener.js;type=application/octet-stream" \ -F "html=@render-screen.js;type=application/octet-stream" \ - -F "html=@socket.io/socket.io.js;type=application/octet-stream" \ -F "html=@index.html;type=application/octet-stream" > /dev/null rm bootstrap-metadata.json } diff --git a/playground/serverless/index.html b/playground/serverless/index.html index 7bcd91fd..cd1084c2 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -13,7 +13,6 @@ height: 400px; } - @@ -32,80 +31,80 @@ hostname = "edge.bohr.cloud"; } - //const socket = io({path: '/api/room/game/websocket'}) - const ws = new WebSocket("wss://" + hostname + "/api/room/game/websocket"); - - ws.addEventListener("open", event => { - console.log('open'); - currentWebSocket = ws; - //ws.send(JSON.stringify({name: username})); - //currentWebSocket.send(JSON.stringify({message: chatInput.value})); - }); - - ws.addEventListener("message", event => { - let data = JSON.parse(event.data); - console.log(data); - }); - - ws.addEventListener("close", event => { - console.log("WebSocket closed, reconnecting:", event.code, event.reason); - //rejoin(); - }); - ws.addEventListener("error", event => { - console.log("WebSocket error, reconnecting:", event); - //rejoin(); - }); - - /* - socket.on('connect', () => { - const playerId = socket.id - console.log(`Player connected on Client with id: ${playerId}`) - - const screen = document.getElementById('screen') - renderScreen(screen, game, requestAnimationFrame, playerId) - }) - - socket.on('setup', (state) => { - const playerId = socket.id - game.setState(state) - - keyboardListener.registerPlayerId(playerId) - keyboardListener.subscribe(game.movePlayer) - keyboardListener.subscribe((command) => { - socket.emit('move-player', command) - }) - }) - - socket.on('add-player', (command) => { - console.log(`Receiving ${command.type} -> ${command.playerId}`) - game.addPlayer(command) - }) - - socket.on('remove-player', (command) => { - console.log(`Receiving ${command.type} -> ${command.playerId}`) - game.removePlayer(command) - }) - - socket.on('move-player', (command) => { - console.log(`Receiving ${command.type} -> ${command.playerId}`) - - const playerId = socket.id - - if (playerId !== command.playerId) { - game.movePlayer(command) - } - }) - - socket.on('add-fruit', (command) => { - console.log(`Receiving ${command.type} -> ${command.fruitId}`) - game.addFruit(command) - }) - - socket.on('remove-fruit', (command) => { - console.log(`Receiving ${command.type} -> ${command.fruitId}`) - game.removeFruit(command) - }) - */ + let startSocket = function() { + currentWebSocket = null; + const ws = new WebSocket("wss://" + hostname + "/api/room/game/websocket"); + + ws.addEventListener("open", event => { + console.log('open'); + currentWebSocket = ws; + + const playerId = Math.random(); //socket.id; + console.log(`Player connected on Client with id: ${playerId}`); + + const screen = document.getElementById('screen'); + renderScreen(screen, game, requestAnimationFrame, playerId); + }); + + ws.addEventListener("message", event => { + let data = JSON.parse(event.data); + let command = data.data; + + console.log(data); + + if (data.emit == 'setup') { + const playerId = Math.random(); //socket.id; + game.setState(state); + keyboardListener.registerPlayerId(playerId); + keyboardListener.subscribe(game.movePlayer); + keyboardListener.subscribe((command) => { + currentWebSocket.send(JSON.stringify({emit: 'move-player', data:chatInput.value})); + }); + } + + if (data.emit == 'add-player') { + console.log(`Receiving ${command.type} -> ${command.playerId}`) + game.addPlayer(command) + } + + if (data.emit == 'remove-player') { + console.log(`Receiving ${command.type} -> ${command.playerId}`) + game.removePlayer(command) + } + + if (data.emit == 'move-player') { + console.log(`Receiving ${command.type} -> ${command.playerId}`) + const playerId = Math.random(); //socket.id; + if (playerId !== command.playerId) { + game.movePlayer(command) + } + } + + if (data.emit == 'add-fruit') { + console.log(`Receiving ${command.type} -> ${command.fruitId}`) + game.addFruit(command) + } + + if (data.emit == 'remove-fruit') { + console.log(`Receiving ${command.type} -> ${command.fruitId}`) + game.removeFruit(command) + } + + }); + + ws.addEventListener("close", event => { + console.log("WebSocket closed, reconnecting:", event.code, event.reason); + startSocket(); + }); + ws.addEventListener("error", event => { + console.log("WebSocket error, reconnecting:", event); + startSocket(); + }); + + } + + startSocket(); + \ No newline at end of file diff --git a/playground/serverless/socket.io/socket.io.js b/playground/serverless/socket.io/socket.io.js deleted file mode 100644 index 270777b6..00000000 --- a/playground/serverless/socket.io/socket.io.js +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * Socket.IO v2.3.0 - * (c) 2014-2019 Guillermo Rauch - * Released under the MIT License. - */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){function r(t,e){"object"==typeof t&&(e=t,t=void 0),e=e||{};var n,r=o(t),i=r.source,u=r.id,p=r.path,h=c[u]&&p in c[u].nsps,f=e.forceNew||e["force new connection"]||!1===e.multiplex||h;return f?(a("ignoring socket cache for %s",i),n=s(i,e)):(c[u]||(a("new io instance for %s",i),c[u]=s(i,e)),n=c[u]),r.query&&!e.query&&(e.query=r.query),n.socket(r.path,e)}var o=n(1),i=n(7),s=n(15),a=n(3)("socket.io-client");t.exports=e=r;var c=e.managers={};e.protocol=i.protocol,e.connect=r,e.Manager=n(15),e.Socket=n(39)},function(t,e,n){function r(t,e){var n=t;e=e||"undefined"!=typeof location&&location,null==t&&(t=e.protocol+"//"+e.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?e.protocol+t:e.host+t),/^(https?|wss?):\/\//.test(t)||(i("protocol-less url %s",t),t="undefined"!=typeof e?e.protocol+"//"+t:"https://"+t),i("parse %s",t),n=o(t)),n.port||(/^(http|ws)$/.test(n.protocol)?n.port="80":/^(http|ws)s$/.test(n.protocol)&&(n.port="443")),n.path=n.path||"/";var r=n.host.indexOf(":")!==-1,s=r?"["+n.host+"]":n.host;return n.id=n.protocol+"://"+s+":"+n.port,n.href=n.protocol+"://"+s+(e&&e.port===n.port?"":":"+n.port),n}var o=n(2),i=n(3)("socket.io-client:url");t.exports=r},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");o!=-1&&i!=-1&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s=n.exec(t||""),a={},c=14;c--;)a[r[c]]=s[c]||"";return o!=-1&&i!=-1&&(a.source=e,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a}},function(t,e,n){(function(r){"use strict";function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type&&!window.process.__nwjs)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(e){if(e[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+e[0]+(this.useColors?"%c ":" ")+"+"+t.exports.humanize(this.diff),this.useColors){var n="color: "+this.color;e.splice(1,0,n,"color: inherit");var r=0,o=0;e[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(r++,"%c"===t&&(o=r))}),e.splice(o,0,n)}}function s(){var t;return"object"===("undefined"==typeof console?"undefined":p(console))&&console.log&&(t=console).log.apply(t,arguments)}function a(t){try{t?e.storage.setItem("debug",t):e.storage.removeItem("debug")}catch(n){}}function c(){var t=void 0;try{t=e.storage.getItem("debug")}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function u(){try{return localStorage}catch(t){}}var p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage=u(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.exports=n(5)(e);var h=t.exports.formatters;h.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}}).call(e,n(4))},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(t){if(p===setTimeout)return setTimeout(t,0);if((p===n||!p)&&setTimeout)return p=setTimeout,setTimeout(t,0);try{return p(t,0)}catch(e){try{return p.call(null,t,0)}catch(e){return p.call(this,t,0)}}}function i(t){if(h===clearTimeout)return clearTimeout(t);if((h===r||!h)&&clearTimeout)return h=clearTimeout,clearTimeout(t);try{return h(t)}catch(e){try{return h.call(null,t)}catch(e){return h.call(this,t)}}}function s(){y&&l&&(y=!1,l.length?d=l.concat(d):m=-1,d.length&&a())}function a(){if(!y){var t=o(s);y=!0;for(var e=d.length;e;){for(l=d,d=[];++m1)for(var n=1;n100)){var e=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*h;case"weeks":case"week":case"w":return n*p;case"days":case"day":case"d":return n*u;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){var e=Math.abs(t);return e>=u?Math.round(t/u)+"d":e>=c?Math.round(t/c)+"h":e>=a?Math.round(t/a)+"m":e>=s?Math.round(t/s)+"s":t+"ms"}function o(t){var e=Math.abs(t);return e>=u?i(t,e,u,"day"):e>=c?i(t,e,c,"hour"):e>=a?i(t,e,a,"minute"):e>=s?i(t,e,s,"second"):t+" ms"}function i(t,e,n,r){var o=e>=1.5*n;return Math.round(t/n)+" "+r+(o?"s":"")}var s=1e3,a=60*s,c=60*a,u=24*c,p=7*u,h=365.25*u;t.exports=function(t,e){e=e||{};var i=typeof t;if("string"===i&&t.length>0)return n(t);if("number"===i&&isFinite(t))return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(){}function o(t){var n=""+t.type;if(e.BINARY_EVENT!==t.type&&e.BINARY_ACK!==t.type||(n+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(n+=t.nsp+","),null!=t.id&&(n+=t.id),null!=t.data){var r=i(t.data);if(r===!1)return g;n+=r}return f("encoded %j as %s",t,n),n}function i(t){try{return JSON.stringify(t)}catch(e){return!1}}function s(t,e){function n(t){var n=d.deconstructPacket(t),r=o(n.packet),i=n.buffers;i.unshift(r),e(i)}d.removeBlobs(t,n)}function a(){this.reconstructor=null}function c(t){var n=0,r={type:Number(t.charAt(0))};if(null==e.types[r.type])return h("unknown packet type "+r.type);if(e.BINARY_EVENT===r.type||e.BINARY_ACK===r.type){for(var o="";"-"!==t.charAt(++n)&&(o+=t.charAt(n),n!=t.length););if(o!=Number(o)||"-"!==t.charAt(n))throw new Error("Illegal attachments");r.attachments=Number(o)}if("/"===t.charAt(n+1))for(r.nsp="";++n;){var i=t.charAt(n);if(","===i)break;if(r.nsp+=i,n===t.length)break}else r.nsp="/";var s=t.charAt(n+1);if(""!==s&&Number(s)==s){for(r.id="";++n;){var i=t.charAt(n);if(null==i||Number(i)!=i){--n;break}if(r.id+=t.charAt(n),n===t.length)break}r.id=Number(r.id)}if(t.charAt(++n)){var a=u(t.substr(n)),c=a!==!1&&(r.type===e.ERROR||y(a));if(!c)return h("invalid payload");r.data=a}return f("decoded %s as %j",t,r),r}function u(t){try{return JSON.parse(t)}catch(e){return!1}}function p(t){this.reconPack=t,this.buffers=[]}function h(t){return{type:e.ERROR,data:"parser error: "+t}}var f=n(8)("socket.io-parser"),l=n(11),d=n(12),y=n(13),m=n(14);e.protocol=4,e.types=["CONNECT","DISCONNECT","EVENT","ACK","ERROR","BINARY_EVENT","BINARY_ACK"],e.CONNECT=0,e.DISCONNECT=1,e.EVENT=2,e.ACK=3,e.ERROR=4,e.BINARY_EVENT=5,e.BINARY_ACK=6,e.Encoder=r,e.Decoder=a;var g=e.ERROR+'"encode error"';r.prototype.encode=function(t,n){if(f("encoding packet %j",t),e.BINARY_EVENT===t.type||e.BINARY_ACK===t.type)s(t,n);else{var r=o(t);n([r])}},l(a.prototype),a.prototype.add=function(t){var n;if("string"==typeof t)n=c(t),e.BINARY_EVENT===n.type||e.BINARY_ACK===n.type?(this.reconstructor=new p(n),0===this.reconstructor.reconPack.attachments&&this.emit("decoded",n)):this.emit("decoded",n);else{if(!m(t)&&!t.base64)throw new Error("Unknown type: "+t);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");n=this.reconstructor.takeBinaryData(t),n&&(this.reconstructor=null,this.emit("decoded",n))}},a.prototype.destroy=function(){this.reconstructor&&this.reconstructor.finishedReconstruction()},p.prototype.takeBinaryData=function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=d.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null},p.prototype.finishedReconstruction=function(){this.reconPack=null,this.buffers=[]}},function(t,e,n){(function(r){"use strict";function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(t){var n=this.useColors;if(t[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+t[0]+(n?"%c ":" ")+"+"+e.humanize(this.diff),n){var r="color: "+this.color;t.splice(1,0,r,"color: inherit");var o=0,i=0;t[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(o++,"%c"===t&&(i=o))}),t.splice(i,0,r)}}function s(){return"object"===("undefined"==typeof console?"undefined":p(console))&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(t){try{null==t?e.storage.removeItem("debug"):e.storage.debug=t}catch(n){}}function c(){var t;try{t=e.storage.debug}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function u(){try{return window.localStorage}catch(t){}}var p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};e=t.exports=n(9),e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:u(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],e.formatters.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}},e.enable(c())}).call(e,n(4))},function(t,e,n){"use strict";function r(t){var n,r=0;for(n in t)r=(r<<5)-r+t.charCodeAt(n),r|=0;return e.colors[Math.abs(r)%e.colors.length]}function o(t){function n(){if(n.enabled){var t=n,r=+new Date,i=r-(o||r);t.diff=i,t.prev=o,t.curr=r,o=r;for(var s=new Array(arguments.length),a=0;a100)){var e=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*p;case"days":case"day":case"d":return n*u;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){return t>=u?Math.round(t/u)+"d":t>=c?Math.round(t/c)+"h":t>=a?Math.round(t/a)+"m":t>=s?Math.round(t/s)+"s":t+"ms"}function o(t){return i(t,u,"day")||i(t,c,"hour")||i(t,a,"minute")||i(t,s,"second")||t+" ms"}function i(t,e,n){if(!(t0)return n(t);if("number"===i&&isNaN(t)===!1)return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(t){if(t)return o(t)}function o(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments)}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,o=0;o0&&!this.encoding){var t=this.packetBuffer.shift();this.packet(t)}},r.prototype.cleanup=function(){p("cleanup");for(var t=this.subs.length,e=0;e=this._reconnectionAttempts)p("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();p("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(p("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(p("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(p("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},r.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},function(t,e,n){t.exports=n(17),t.exports.parser=n(24)},function(t,e,n){function r(t,e){return this instanceof r?(e=e||{},t&&"object"==typeof t&&(e=t,t=null),t?(t=p(t),e.hostname=t.host,e.secure="https"===t.protocol||"wss"===t.protocol,e.port=t.port,t.query&&(e.query=t.query)):e.host&&(e.hostname=p(e.host).host),this.secure=null!=e.secure?e.secure:"undefined"!=typeof location&&"https:"===location.protocol,e.hostname&&!e.port&&(e.port=this.secure?"443":"80"),this.agent=e.agent||!1,this.hostname=e.hostname||("undefined"!=typeof location?location.hostname:"localhost"),this.port=e.port||("undefined"!=typeof location&&location.port?location.port:this.secure?443:80),this.query=e.query||{},"string"==typeof this.query&&(this.query=h.decode(this.query)),this.upgrade=!1!==e.upgrade,this.path=(e.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!e.forceJSONP,this.jsonp=!1!==e.jsonp,this.forceBase64=!!e.forceBase64,this.enablesXDR=!!e.enablesXDR,this.withCredentials=!1!==e.withCredentials,this.timestampParam=e.timestampParam||"t",this.timestampRequests=e.timestampRequests,this.transports=e.transports||["polling","websocket"],this.transportOptions=e.transportOptions||{},this.readyState="",this.writeBuffer=[],this.prevBufferLen=0,this.policyPort=e.policyPort||843,this.rememberUpgrade=e.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=e.onlyBinaryUpgrades,this.perMessageDeflate=!1!==e.perMessageDeflate&&(e.perMessageDeflate||{}),!0===this.perMessageDeflate&&(this.perMessageDeflate={}),this.perMessageDeflate&&null==this.perMessageDeflate.threshold&&(this.perMessageDeflate.threshold=1024),this.pfx=e.pfx||null,this.key=e.key||null,this.passphrase=e.passphrase||null,this.cert=e.cert||null,this.ca=e.ca||null,this.ciphers=e.ciphers||null,this.rejectUnauthorized=void 0===e.rejectUnauthorized||e.rejectUnauthorized,this.forceNode=!!e.forceNode,this.isReactNative="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),("undefined"==typeof self||this.isReactNative)&&(e.extraHeaders&&Object.keys(e.extraHeaders).length>0&&(this.extraHeaders=e.extraHeaders),e.localAddress&&(this.localAddress=e.localAddress)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingIntervalTimer=null,this.pingTimeoutTimer=null,void this.open()):new r(t,e)}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=n(18),s=n(11),a=n(3)("engine.io-client:socket"),c=n(38),u=n(24),p=n(2),h=n(32);t.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=u.protocol,r.Socket=r,r.Transport=n(23),r.transports=n(18),r.parser=n(24),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=u.protocol,e.transport=t;var n=this.transportOptions[t]||{};this.id&&(e.sid=this.id);var r=new i[t]({query:e,socket:this,agent:n.agent||this.agent,hostname:n.hostname||this.hostname,port:n.port||this.port,secure:n.secure||this.secure,path:n.path||this.path,forceJSONP:n.forceJSONP||this.forceJSONP,jsonp:n.jsonp||this.jsonp,forceBase64:n.forceBase64||this.forceBase64,enablesXDR:n.enablesXDR||this.enablesXDR,withCredentials:n.withCredentials||this.withCredentials,timestampRequests:n.timestampRequests||this.timestampRequests,timestampParam:n.timestampParam||this.timestampParam,policyPort:n.policyPort||this.policyPort,pfx:n.pfx||this.pfx,key:n.key||this.key,passphrase:n.passphrase||this.passphrase,cert:n.cert||this.cert,ca:n.ca||this.ca,ciphers:n.ciphers||this.ciphers,rejectUnauthorized:n.rejectUnauthorized||this.rejectUnauthorized,perMessageDeflate:n.perMessageDeflate||this.perMessageDeflate,extraHeaders:n.extraHeaders||this.extraHeaders,forceNode:n.forceNode||this.forceNode,localAddress:n.localAddress||this.localAddress,requestTimeout:n.requestTimeout||this.requestTimeout,protocols:n.protocols||void 0,isReactNative:this.isReactNative});return r},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1)t="websocket";else{ -if(0===this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(f.onlyBinaryUpgrades){var e=!this.supportsBinary&&f.transport.supportsBinary;h=h||e}h||(a('probe transport "%s" opened',t),p.send([{type:"ping",data:"probe"}]),p.once("packet",function(e){if(!h)if("pong"===e.type&&"probe"===e.data){if(a('probe transport "%s" pong',t),f.upgrading=!0,f.emit("upgrading",p),!p)return;r.priorWebsocketSuccess="websocket"===p.name,a('pausing current transport "%s"',f.transport.name),f.transport.pause(function(){h||"closed"!==f.readyState&&(a("changing transport and sending upgrade packet"),u(),f.setTransport(p),p.send([{type:"upgrade"}]),f.emit("upgrade",p),p=null,f.upgrading=!1,f.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=p.name,f.emit("upgradeError",n)}}))}function n(){h||(h=!0,u(),p.close(),p=null)}function o(e){var r=new Error("probe error: "+e);r.transport=p.name,n(),a('probe transport "%s" failed because of error: %s',t,e),f.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){p&&t.name!==p.name&&(a('"%s" works - aborting "%s"',t.name,p.name),n())}function u(){p.removeListener("open",e),p.removeListener("error",o),p.removeListener("close",i),f.removeListener("close",s),f.removeListener("upgrading",c)}a('probing transport "%s"',t);var p=this.createTransport(t,{probe:1}),h=!1,f=this;r.priorWebsocketSuccess=!1,p.once("open",e),p.once("error",o),p.once("close",i),this.once("close",s),this.once("upgrading",c),p.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;t1?{type:b[o],data:t.substring(1)}:{type:b[o]}:C}var i=new Uint8Array(t),o=i[0],s=f(t,1);return w&&"blob"===n&&(s=new w([s])),{type:b[o],data:s}},e.decodeBase64Packet=function(t,e){var n=b[t.charAt(0)];if(!u)return{type:n,data:{base64:!0,data:t.substr(1)}};var r=u.decode(t.substr(1));return"blob"===e&&w&&(r=new w([r])),{type:n,data:r}},e.encodePayload=function(t,n,r){function o(t){return t.length+":"+t}function i(t,r){e.encodePacket(t,!!s&&n,!1,function(t){r(null,o(t))})}"function"==typeof n&&(r=n,n=null);var s=h(t);return n&&s?w&&!g?e.encodePayloadAsBlob(t,r):e.encodePayloadAsArrayBuffer(t,r):t.length?void c(t,i,function(t,e){return r(e.join(""))}):r("0:")},e.decodePayload=function(t,n,r){if("string"!=typeof t)return e.decodePayloadAsBinary(t,n,r);"function"==typeof n&&(r=n,n=null);var o;if(""===t)return r(C,0,1);for(var i,s,a="",c=0,u=t.length;c0;){for(var s=new Uint8Array(o),a=0===s[0],c="",u=1;255!==s[u];u++){if(c.length>310)return r(C,0,1);c+=s[u]}o=f(o,2+c.length),c=parseInt(c);var p=f(o,0,c);if(a)try{p=String.fromCharCode.apply(null,new Uint8Array(p))}catch(h){var l=new Uint8Array(p);p="";for(var u=0;ur&&(n=r),e>=r||e>=n||0===r)return new ArrayBuffer(0);for(var o=new Uint8Array(t),i=new Uint8Array(n-e),s=e,a=0;s=55296&&e<=56319&&o65535&&(e-=65536,o+=d(e>>>10&1023|55296),e=56320|1023&e),o+=d(e);return o}function o(t,e){if(t>=55296&&t<=57343){if(e)throw Error("Lone surrogate U+"+t.toString(16).toUpperCase()+" is not a scalar value");return!1}return!0}function i(t,e){return d(t>>e&63|128)}function s(t,e){if(0==(4294967168&t))return d(t);var n="";return 0==(4294965248&t)?n=d(t>>6&31|192):0==(4294901760&t)?(o(t,e)||(t=65533),n=d(t>>12&15|224),n+=i(t,6)):0==(4292870144&t)&&(n=d(t>>18&7|240),n+=i(t,12),n+=i(t,6)),n+=d(63&t|128)}function a(t,e){e=e||{};for(var r,o=!1!==e.strict,i=n(t),a=i.length,c=-1,u="";++c=f)throw Error("Invalid byte index");var t=255&h[l];if(l++,128==(192&t))return 63&t;throw Error("Invalid continuation byte")}function u(t){var e,n,r,i,s;if(l>f)throw Error("Invalid byte index");if(l==f)return!1;if(e=255&h[l],l++,0==(128&e))return e;if(192==(224&e)){if(n=c(),s=(31&e)<<6|n,s>=128)return s;throw Error("Invalid continuation byte")}if(224==(240&e)){if(n=c(),r=c(),s=(15&e)<<12|n<<6|r,s>=2048)return o(s,t)?s:65533;throw Error("Invalid continuation byte")}if(240==(248&e)&&(n=c(),r=c(),i=c(),s=(7&e)<<18|n<<12|r<<6|i,s>=65536&&s<=1114111))return s;throw Error("Invalid UTF-8 detected")}function p(t,e){e=e||{};var o=!1!==e.strict;h=n(t),f=h.length,l=0;for(var i,s=[];(i=u(o))!==!1;)s.push(i);return r(s)}/*! https://mths.be/utf8js v2.1.2 by @mathias */ -var h,f,l,d=String.fromCharCode;t.exports={version:"2.1.2",encode:a,decode:p}},function(t,e){!function(){"use strict";for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(256),r=0;r>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3===2?i=i.substring(0,i.length-1)+"=":o%3===1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(t){var e,r,o,i,s,a=.75*t.length,c=t.length,u=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var p=new ArrayBuffer(a),h=new Uint8Array(p);for(e=0;e>4,h[u++]=(15&o)<<4|i>>2,h[u++]=(3&i)<<6|63&s;return p}}()},function(t,e){function n(t){return t.map(function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var n=new Uint8Array(t.byteLength);n.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=n.buffer}return e}return t})}function r(t,e){e=e||{};var r=new i;return n(t).forEach(function(t){r.append(t)}),e.type?r.getBlob(e.type):r.getBlob()}function o(t,e){return new Blob(n(t),e||{})}var i="undefined"!=typeof i?i:"undefined"!=typeof WebKitBlobBuilder?WebKitBlobBuilder:"undefined"!=typeof MSBlobBuilder?MSBlobBuilder:"undefined"!=typeof MozBlobBuilder&&MozBlobBuilder,s=function(){try{var t=new Blob(["hi"]);return 2===t.size}catch(e){return!1}}(),a=s&&function(){try{var t=new Blob([new Uint8Array([1,2])]);return 2===t.size}catch(e){return!1}}(),c=i&&i.prototype.append&&i.prototype.getBlob;"undefined"!=typeof Blob&&(r.prototype=Blob.prototype,o.prototype=Blob.prototype),t.exports=function(){return s?a?Blob:o:c?r:void 0}()},function(t,e){e.encode=function(t){var e="";for(var n in t)t.hasOwnProperty(n)&&(e.length&&(e+="&"),e+=encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e},e.decode=function(t){for(var e={},n=t.split("&"),r=0,o=n.length;r0);return e}function r(t){var e=0;for(p=0;p';i=document.createElement(e)}catch(t){i=document.createElement("iframe"),i.name=o.iframeId,i.src="javascript:0"}i.id=o.iframeId,o.form.appendChild(i),o.iframe=i}var o=this;if(!this.form){var i,s=document.createElement("form"),a=document.createElement("textarea"),c=this.iframeId="eio_iframe_"+this.index;s.className="socketio",s.style.position="absolute",s.style.top="-1000px",s.style.left="-1000px",s.target=c,s.method="POST",s.setAttribute("accept-charset","utf-8"),a.name="d",s.appendChild(a),document.body.appendChild(s),this.form=s,this.area=a}this.form.action=this.uri(),r(),t=t.replace(p,"\\\n"),this.area.value=t.replace(u,"\\n");try{this.form.submit()}catch(h){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===o.iframe.readyState&&n()}:this.iframe.onload=n}}).call(e,function(){return this}())},function(t,e,n){function r(t){var e=t&&t.forceBase64;e&&(this.supportsBinary=!1),this.perMessageDeflate=t.perMessageDeflate,this.usingBrowserWebSocket=o&&!t.forceNode,this.protocols=t.protocols,this.usingBrowserWebSocket||(l=i),s.call(this,t)}var o,i,s=n(23),a=n(24),c=n(32),u=n(33),p=n(34),h=n(3)("engine.io-client:websocket");if("undefined"!=typeof WebSocket?o=WebSocket:"undefined"!=typeof self&&(o=self.WebSocket||self.MozWebSocket),"undefined"==typeof window)try{i=n(37)}catch(f){}var l=o||i;t.exports=r,u(r,s),r.prototype.name="websocket",r.prototype.supportsBinary=!0,r.prototype.doOpen=function(){if(this.check()){var t=this.uri(),e=this.protocols,n={agent:this.agent,perMessageDeflate:this.perMessageDeflate};n.pfx=this.pfx,n.key=this.key,n.passphrase=this.passphrase,n.cert=this.cert,n.ca=this.ca,n.ciphers=this.ciphers,n.rejectUnauthorized=this.rejectUnauthorized,this.extraHeaders&&(n.headers=this.extraHeaders),this.localAddress&&(n.localAddress=this.localAddress);try{this.ws=this.usingBrowserWebSocket&&!this.isReactNative?e?new l(t,e):new l(t):new l(t,e,n)}catch(r){return this.emit("error",r)}void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.supports&&this.ws.supports.binary?(this.supportsBinary=!0,this.ws.binaryType="nodebuffer"):this.ws.binaryType="arraybuffer",this.addEventListeners()}},r.prototype.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.onOpen()},this.ws.onclose=function(){t.onClose()},this.ws.onmessage=function(e){t.onData(e.data)},this.ws.onerror=function(e){t.onError("websocket error",e)}},r.prototype.write=function(t){function e(){n.emit("flush"),setTimeout(function(){n.writable=!0,n.emit("drain")},0)}var n=this;this.writable=!1;for(var r=t.length,o=0,i=r;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])}); -//# sourceMappingURL=socket.io.js.map \ No newline at end of file diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 9c866dd0..62e21b7c 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -39,7 +39,7 @@ export default { }); } } -/* + async function handleApiRequest(path, request, env) { switch (path[0]) { @@ -75,7 +75,7 @@ async function handleApiRequest(path, request, env) { return new Response("Not found", {status: 404}); } } -*/ + export class ChatRoom { constructor(controller, env) { this.storage = controller.storage; From 031a815fda1eb3051fd9fe4b1d0e2f8522d6fafe Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 03:33:24 -0300 Subject: [PATCH 57/72] . --- playground/serverless/worker.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 62e21b7c..93e52f85 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -2,7 +2,6 @@ import HTML from "index.html"; import game from "game.js"; import keyboard_listener from "keyboard-listener.js"; import render_screen from "render-screen.js"; -import socket_io from "socket.io.js"; async function handleErrors(request, func) { try { From e26ff655ac0f744c34dd3235c1f92e5ee8f14b6b Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 03:38:21 -0300 Subject: [PATCH 58/72] . --- playground/serverless/worker.mjs | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 93e52f85..03c6d226 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -82,7 +82,7 @@ export class ChatRoom { this.sessions = []; } - createGame(storage) { + createGame(parent) { const state = { players: {}, fruits: {}, @@ -93,7 +93,7 @@ export class ChatRoom { } async function saveState() { - await storage.put("GAME", state); + await parent.storage.put("GAME", state); } function start() { @@ -102,7 +102,7 @@ export class ChatRoom { } function notifyAll(command) { - broadcast(command); + parent.broadcast(command); } function setState(newState) { @@ -245,26 +245,9 @@ export class ChatRoom { return await handleErrors(request, async () => { let url = new URL(request.url); if (url.pathname.indexOf('/websocket') != -1) { - // The request is to `/api/room//websocket`. A client is trying to establish a new - // WebSocket session. - if (request.headers.get("Upgrade") != "websocket") { - return new Response("expected websocket", {status: 400}); - } - - // Get the client's IP address for use with the rate limiter. - let ip = request.headers.get("CF-Connecting-IP"); - - // To accept the WebSocket request, we create a WebSocketPair (which is like a socketpair, - // i.e. two WebSockets that talk to each other), we return one end of the pair in the - // response, and we operate on the other end. Note that this API is not part of the - // Fetch API standard; unfortunately, the Fetch API / Service Workers specs do not define - // any way to act as a WebSocket server today. + if (request.headers.get("Upgrade") != "websocket") return new Response("expected websocket", {status: 400}); let pair = new WebSocketPair(); - - // We're going to take pair[1] as our end, and return pair[0] to the client. - await this.handleSession(pair[1], ip); - - // Now we return the other end of the pair to the client. + await this.handleSession(pair[1]); return new Response(null, { status: 101, webSocket: pair[0] }); } else { return new Response("Not found", {status: 404}); @@ -272,14 +255,14 @@ export class ChatRoom { }); } - async handleSession(webSocket, ip) { + async handleSession(webSocket) { webSocket.accept(); let session = {webSocket}; this.sessions.push(session); - const game = this.createGame(this.storage); + const game = this.createGame(this); const game_data = await this.storage.get("GAME"); if (game_data != null) game.setState(game_data); game.start(); From 19f5995874150281e0e8c1289ae0de464787cf37 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 03:48:44 -0300 Subject: [PATCH 59/72] . --- playground/serverless/index.html | 5 +++-- playground/serverless/worker.mjs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/playground/serverless/index.html b/playground/serverless/index.html index cd1084c2..fb533eb0 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -47,6 +47,7 @@ }); ws.addEventListener("message", event => { + console.log(event.data); let data = JSON.parse(event.data); let command = data.data; @@ -54,11 +55,11 @@ if (data.emit == 'setup') { const playerId = Math.random(); //socket.id; - game.setState(state); + game.setState(command); keyboardListener.registerPlayerId(playerId); keyboardListener.subscribe(game.movePlayer); keyboardListener.subscribe((command) => { - currentWebSocket.send(JSON.stringify({emit: 'move-player', data:chatInput.value})); + currentWebSocket.send(JSON.stringify({emit: 'move-player', data:command})); }); } diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 03c6d226..addc7fe2 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -282,7 +282,7 @@ export class ChatRoom { if (data.emit == 'move-player') { data.data.playerId = playerId; data.data.type = 'move-player'; - await game.movePlayer(data.data); + await game.movePlayer(JSON.parse(data.data)); } } catch (err) { webSocket.send(JSON.stringify({error: err.stack})); From a5148520b3d9661dd8163d0536ab0356211ad62e Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 03:53:45 -0300 Subject: [PATCH 60/72] . --- playground/serverless/worker.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index addc7fe2..9f4f0691 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -280,9 +280,10 @@ export class ChatRoom { } let data = JSON.parse(msg.data); if (data.emit == 'move-player') { + data.data = JSON.parse(data.data); data.data.playerId = playerId; data.data.type = 'move-player'; - await game.movePlayer(JSON.parse(data.data)); + await game.movePlayer(data.data); } } catch (err) { webSocket.send(JSON.stringify({error: err.stack})); From b6f77119218cc92033f435d8aef36c44e4057968 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 03:55:41 -0300 Subject: [PATCH 61/72] . --- playground/serverless/worker.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 9f4f0691..03c6d226 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -280,7 +280,6 @@ export class ChatRoom { } let data = JSON.parse(msg.data); if (data.emit == 'move-player') { - data.data = JSON.parse(data.data); data.data.playerId = playerId; data.data.type = 'move-player'; await game.movePlayer(data.data); From 5d21051cf4f2fb7c7669630ce1f6a7b2a741768a Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 03:56:27 -0300 Subject: [PATCH 62/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 03c6d226..d2e27a39 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -302,7 +302,7 @@ export class ChatRoom { let quitters = []; this.sessions = this.sessions.filter(session => { try { - session.webSocket.send(message); + session.webSocket.send(JSON.stringify(message)); return true; } catch (err) { session.quit = true; From f2e06cb26a55680188a48edd3aaf12b79b8a6cf0 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 04:06:02 -0300 Subject: [PATCH 63/72] . --- playground/serverless/index.html | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/playground/serverless/index.html b/playground/serverless/index.html index fb533eb0..573a9969 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -47,15 +47,11 @@ }); ws.addEventListener("message", event => { - console.log(event.data); - let data = JSON.parse(event.data); - let command = data.data; - - console.log(data); + let command = JSON.parse(event.data); - if (data.emit == 'setup') { + if (command.emit == 'setup') { const playerId = Math.random(); //socket.id; - game.setState(command); + game.setState(command.data); keyboardListener.registerPlayerId(playerId); keyboardListener.subscribe(game.movePlayer); keyboardListener.subscribe((command) => { @@ -63,17 +59,17 @@ }); } - if (data.emit == 'add-player') { + if (command.type == 'add-player') { console.log(`Receiving ${command.type} -> ${command.playerId}`) game.addPlayer(command) } - if (data.emit == 'remove-player') { + if (command.type == 'remove-player') { console.log(`Receiving ${command.type} -> ${command.playerId}`) game.removePlayer(command) } - if (data.emit == 'move-player') { + if (command.type == 'move-player') { console.log(`Receiving ${command.type} -> ${command.playerId}`) const playerId = Math.random(); //socket.id; if (playerId !== command.playerId) { @@ -81,12 +77,12 @@ } } - if (data.emit == 'add-fruit') { + if (command.type == 'add-fruit') { console.log(`Receiving ${command.type} -> ${command.fruitId}`) game.addFruit(command) } - if (data.emit == 'remove-fruit') { + if (command.type == 'remove-fruit') { console.log(`Receiving ${command.type} -> ${command.fruitId}`) game.removeFruit(command) } From 32f032f04d9a58c4b8d7a38bf2246140e204a41f Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 04:18:13 -0300 Subject: [PATCH 64/72] . --- playground/serverless/index.html | 10 +++------- playground/serverless/worker.mjs | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/playground/serverless/index.html b/playground/serverless/index.html index 573a9969..8955f21e 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -26,6 +26,7 @@ const keyboardListener = createKeyboardListener(document) let currentWebSocket = null; + let playerId = null; let hostname = window.location.host; if (hostname == "localhost") { hostname = "edge.bohr.cloud"; @@ -33,24 +34,19 @@ let startSocket = function() { currentWebSocket = null; + playerId = null; const ws = new WebSocket("wss://" + hostname + "/api/room/game/websocket"); ws.addEventListener("open", event => { console.log('open'); currentWebSocket = ws; - - const playerId = Math.random(); //socket.id; - console.log(`Player connected on Client with id: ${playerId}`); - - const screen = document.getElementById('screen'); - renderScreen(screen, game, requestAnimationFrame, playerId); }); ws.addEventListener("message", event => { let command = JSON.parse(event.data); if (command.emit == 'setup') { - const playerId = Math.random(); //socket.id; + playerId = command.playerId; game.setState(command.data); keyboardListener.registerPlayerId(playerId); keyboardListener.subscribe(game.movePlayer); diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index d2e27a39..30004cde 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -270,7 +270,7 @@ export class ChatRoom { const playerId = Math.random(); await game.addPlayer({ playerId: playerId }); - webSocket.send(JSON.stringify({emit: "setup", data:game.state})); + webSocket.send(JSON.stringify({emit: "setup", data:game.state, playerId:playerId})); webSocket.addEventListener("message", async msg => { try { From db53ec0551ab65422435f624b26f0cf8a8f56af4 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 04:18:59 -0300 Subject: [PATCH 65/72] . --- playground/serverless/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/playground/serverless/index.html b/playground/serverless/index.html index 8955f21e..f493cbed 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -67,7 +67,6 @@ if (command.type == 'move-player') { console.log(`Receiving ${command.type} -> ${command.playerId}`) - const playerId = Math.random(); //socket.id; if (playerId !== command.playerId) { game.movePlayer(command) } From 88099518671020ff19a6e927bbdbd10bf1ad0298 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 04:28:34 -0300 Subject: [PATCH 66/72] . --- playground/serverless/index.html | 2 ++ playground/serverless/worker.mjs | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/playground/serverless/index.html b/playground/serverless/index.html index f493cbed..2d4ce506 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -44,8 +44,10 @@ ws.addEventListener("message", event => { let command = JSON.parse(event.data); + console.log(command); if (command.emit == 'setup') { + console.log('setup'); playerId = command.playerId; game.setState(command.data); keyboardListener.registerPlayerId(playerId); diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 30004cde..aada8dd4 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -255,6 +255,12 @@ export class ChatRoom { }); } + getRandomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; + } + async handleSession(webSocket) { webSocket.accept(); @@ -267,7 +273,7 @@ export class ChatRoom { if (game_data != null) game.setState(game_data); game.start(); - const playerId = Math.random(); + const playerId = getRandomInt(0, Number.MAX_SAFE_INTEGER); await game.addPlayer({ playerId: playerId }); webSocket.send(JSON.stringify({emit: "setup", data:game.state, playerId:playerId})); From 84bb43ef2ce05de91be059e8a2aa44887e785124 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 04:30:35 -0300 Subject: [PATCH 67/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index aada8dd4..54e3913a 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -273,7 +273,7 @@ export class ChatRoom { if (game_data != null) game.setState(game_data); game.start(); - const playerId = getRandomInt(0, Number.MAX_SAFE_INTEGER); + const playerId = this.getRandomInt(0, Number.MAX_SAFE_INTEGER); await game.addPlayer({ playerId: playerId }); webSocket.send(JSON.stringify({emit: "setup", data:game.state, playerId:playerId})); From a31487ab5e563390e7ff0c184db22601bfaadefc Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 04:34:18 -0300 Subject: [PATCH 68/72] . --- playground/serverless/index.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playground/serverless/index.html b/playground/serverless/index.html index 2d4ce506..5b26b364 100644 --- a/playground/serverless/index.html +++ b/playground/serverless/index.html @@ -55,11 +55,14 @@ keyboardListener.subscribe((command) => { currentWebSocket.send(JSON.stringify({emit: 'move-player', data:command})); }); + const screen = document.getElementById('screen'); + renderScreen(screen, game, requestAnimationFrame, playerId); } if (command.type == 'add-player') { console.log(`Receiving ${command.type} -> ${command.playerId}`) - game.addPlayer(command) + if (playerId == null) playerId = command.playerId; + game.addPlayer(command); } if (command.type == 'remove-player') { From 60d7ecf832dfbd74b6ae5991b6fc4c949a747f2a Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 04:37:36 -0300 Subject: [PATCH 69/72] . --- playground/serverless/worker.mjs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 54e3913a..48533d68 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -93,7 +93,7 @@ export class ChatRoom { } async function saveState() { - await parent.storage.put("GAME", state); + await parent.storage.put("GAME_EDGE", state); } function start() { @@ -265,15 +265,17 @@ export class ChatRoom { webSocket.accept(); - let session = {webSocket}; + const playerId = this.getRandomInt(0, Number.MAX_SAFE_INTEGER); + + let session = {webSocket, playerId}; this.sessions.push(session); const game = this.createGame(this); - const game_data = await this.storage.get("GAME"); + const game_data = await this.storage.get("GAME_EDGE"); if (game_data != null) game.setState(game_data); game.start(); - const playerId = this.getRandomInt(0, Number.MAX_SAFE_INTEGER); + await game.addPlayer({ playerId: playerId }); webSocket.send(JSON.stringify({emit: "setup", data:game.state, playerId:playerId})); @@ -317,9 +319,7 @@ export class ChatRoom { } }); quitters.forEach(quitter => { - if (quitter.name) { - this.broadcast({quit: quitter.name}); - } + await game.removePlayer({ playerId: quitter.playerId }); }); } } From d40e18eb9c166486d2df00d5c2dd36e4a8531ed6 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 04:40:46 -0300 Subject: [PATCH 70/72] . --- playground/serverless/worker.mjs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index 48533d68..d30ca5c8 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -101,8 +101,8 @@ export class ChatRoom { setInterval(addFruit, frequency) } - function notifyAll(command) { - parent.broadcast(command); + async function notifyAll(command) { + await parent.broadcast(command); } function setState(newState) { @@ -119,7 +119,7 @@ export class ChatRoom { y: playerY } - notifyAll({ + await notifyAll({ type: 'add-player', playerId: playerId, playerX: playerX, @@ -134,7 +134,7 @@ export class ChatRoom { delete state.players[playerId] - notifyAll({ + await notifyAll({ type: 'remove-player', playerId: playerId }) @@ -153,7 +153,7 @@ export class ChatRoom { y: fruitY } - notifyAll({ + await notifyAll({ type: 'add-fruit', fruitId: fruitId, fruitX: fruitX, @@ -169,7 +169,7 @@ export class ChatRoom { delete state.fruits[fruitId] - notifyAll({ + await notifyAll({ type: 'remove-fruit', fruitId: fruitId, }) @@ -178,7 +178,7 @@ export class ChatRoom { } async function movePlayer(command) { - notifyAll(command) + await notifyAll(command) const acceptedMoves = { ArrowUp(player) { @@ -306,7 +306,7 @@ export class ChatRoom { webSocket.addEventListener("error", closeOrErrorHandler); } - broadcast(message) { + async broadcast(message) { let quitters = []; this.sessions = this.sessions.filter(session => { try { From d3654e8ff10489d3bbc9e229b41d6b3d33fbdd25 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Tue, 1 Dec 2020 04:42:16 -0300 Subject: [PATCH 71/72] . --- playground/serverless/worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/serverless/worker.mjs b/playground/serverless/worker.mjs index d30ca5c8..c28192ee 100644 --- a/playground/serverless/worker.mjs +++ b/playground/serverless/worker.mjs @@ -318,7 +318,7 @@ export class ChatRoom { return false; } }); - quitters.forEach(quitter => { + quitters.forEach(async quitter => { await game.removePlayer({ playerId: quitter.playerId }); }); } From de32a90be4f63734c164abb636d66e161d26f278 Mon Sep 17 00:00:00 2001 From: Lucas Boemeke Date: Fri, 11 Dec 2020 15:01:05 -0300 Subject: [PATCH 72/72] . --- {.github => playground/serverless/.github}/workflows/main.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.github => playground/serverless/.github}/workflows/main.yml (100%) diff --git a/.github/workflows/main.yml b/playground/serverless/.github/workflows/main.yml similarity index 100% rename from .github/workflows/main.yml rename to playground/serverless/.github/workflows/main.yml