-
Notifications
You must be signed in to change notification settings - Fork 627
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #51 from boemekeld/master
Versão stateful serverless com Cloudflare Worker e Durable Objects
- Loading branch information
Showing
6 changed files
with
798 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.