Skip to content

Commit

Permalink
Merge pull request #51 from boemekeld/master
Browse files Browse the repository at this point in the history
Versão stateful serverless com Cloudflare Worker e Durable Objects
  • Loading branch information
filipedeschamps authored Dec 11, 2020
2 parents 8bfca3f + de32a90 commit 7c6a789
Show file tree
Hide file tree
Showing 6 changed files with 798 additions and 0 deletions.
145 changes: 145 additions & 0 deletions playground/serverless/.github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: Deploy

on:
push:
branches:
- master

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"
working-directory: playground/serverless
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 "[email protected];type=application/json" \
-F "[email protected];type=application/javascript+module" \
-F "[email protected];type=application/octet-stream" \
-F "[email protected];type=application/octet-stream" \
-F "[email protected];type=application/octet-stream" \
-F "[email protected];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": "worker.mjs"}' > bootstrap-metadata.json
curl_api https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/scripts/$SCRIPT_NAME \
-X PUT \
-F "[email protected];type=application/json" \
-F "[email protected];type=application/javascript+module" \
-F "[email protected];type=application/octet-stream" \
-F "[email protected];type=application/octet-stream" \
-F "[email protected];type=application/octet-stream" \
-F "[email protected];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": "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."
154 changes: 154 additions & 0 deletions playground/serverless/game.js
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 7c6a789

Please sign in to comment.