Skip to content

added Try Valkey support #255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions content/try-valkey/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
+++
title = "Try Valkey"
template = "valkey-try-me.html"
+++

1 change: 1 addition & 0 deletions templates/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<a role="menuitem" href="/participants/">Participants</a>
</div>
</div>
<a role="menuitem" href="/try-valkey/">Try Valkey</a>
</nav>
</div>
</div>
Expand Down
277 changes: 277 additions & 0 deletions templates/valkey-try-me.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
{% extends "fullwidth.html" %}
{%- block head -%}
<title>Try Valkey</title>

<!-- Scripts -->
<script src="https://dtq9kvai1345o.cloudfront.net/vos/v86/libv86.js"></script>
<script src="https://dtq9kvai1345o.cloudfront.net/vos/xterm/xterm.min.js"></script>
<script src="https://dtq9kvai1345o.cloudfront.net/vos/pako/pako.min.js"></script>
<script src="https://dtq9kvai1345o.cloudfront.net/vos/v86/serial_xterm.js"></script>

<!-- Styles -->
<link rel="stylesheet" href="https://dtq9kvai1345o.cloudfront.net/vos/xterm/xterm.css" />

<style>
/* Container Styles */
.container {
background: rgb(47, 47, 47);
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
padding: 10px;
overflow-y: hidden;
align-items: center;
}

#terminal-container {
width: 100%;
height: 100%;
background: #000;
color: #fff;
border: none;
overflow-y: hidden;
overflow-x: hidden;
}

#loadingContainer {
text-align: center;
margin-top: 20px;
}

#progressText {
font-size: 18px;
margin-bottom: 10px;
}

#progressBar {
width: 80%;
height: 20px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #e9ecef;
}
</style>
{%- endblock -%}
{% block main_content %}
<p>This is an in-browser Valkey server and CLI that runs directly within your browser using a V86 emulator, requiring no external installations. </p>
<p>Try it out below:</p>
<div id="terminalWrapper" class="container" style="display: none;">
<div id="terminal-container"></div>
</div>
<!-- Warning Section -->
<div id="warningContainer" style="text-align: center; margin-top: 20px;">
<button id="startButton" style="padding: 10px 20px; font-size: 18px; margin-top: 10px;">Load Emulator</button>
<p>This emulator will download approximately 50MB of data.</p>
</div>
<!-- Loading Section (Hidden at first) -->
<div id="loadingContainer" style="display: none;">
<p id="progressText">Preparing to load...</p>
<progress id="progressBar" value="0" max="100"></progress>
</div>



<script>
"use strict";
const FILE_URL = "https://dtq9kvai1345o.cloudfront.net/8.1.0/states/state.bin.gz"; // Path to the .gz file
const CACHE_KEY = "valkey_binary_cache";
const LAST_MODIFIED_KEY = "valkey_last_modified";
let emulator;

// Open or create IndexedDB
async function openIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("binaryCacheDB", 1);

request.onerror = () => reject("Error opening IndexedDB");
request.onsuccess = () => resolve(request.result);

request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore("cache", { keyPath: "key" });
};
});
}

// Retrieve binary from cache
async function getCachedBinary(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(["cache"], "readonly");
const objectStore = transaction.objectStore("cache");
const request = objectStore.get(CACHE_KEY);

request.onerror = () => reject("Error retrieving cached binary");
request.onsuccess = () => resolve(request.result ? request.result.data : null);
});
}

// Save binary to cache
async function saveBinaryToCache(db, data) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(["cache"], "readwrite");
const objectStore = transaction.objectStore("cache");
const request = objectStore.put({ key: CACHE_KEY, data });

request.onerror = () => reject("Error saving binary to cache");
request.onsuccess = () => resolve();
});
}

// Check if binary is updated
async function checkIfUpdated() {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("HEAD", FILE_URL, true);

xhr.onload = () => {
const serverLastModified = xhr.getResponseHeader("Last-Modified");
const cachedLastModified = localStorage.getItem(LAST_MODIFIED_KEY);

if (!serverLastModified || serverLastModified !== cachedLastModified) {
localStorage.setItem(LAST_MODIFIED_KEY, serverLastModified);
resolve(true);
} else {
resolve(false);
}
};

xhr.onerror = () => reject("Error checking file version");
xhr.send();
});
}

// Download and decompress binary
function downloadAndDecompressBinary(callback) {
const xhr = new XMLHttpRequest();
xhr.open("GET", FILE_URL, true);
xhr.responseType = "arraybuffer";

xhr.onprogress = (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
document.getElementById("progressBar").value = percentComplete;
}
};

xhr.onload = () => {
if (xhr.status === 200) {
document.getElementById("progressText").innerText = "Decompressing image...";
const decompressedData = pako.ungzip(new Uint8Array(xhr.response));
callback(decompressedData);
}
};

xhr.onerror = () => {
document.getElementById("progressText").innerText = "Download failed!";
};

xhr.send();
}

async function loadEmulator(decompressedData) {
const progressText = document.getElementById("progressText");

const blob = new Blob([decompressedData], { type: "application/octet-stream" });
const imgUrl = URL.createObjectURL(blob);

progressText.innerText = "Starting emulator...";

emulator = new V86({
wasm_path: "https://dtq9kvai1345o.cloudfront.net/vos/v86/v86.wasm",
memory_size: 512 * 1024 * 1024,
bios: { url: "https://dtq9kvai1345o.cloudfront.net/vos/v86/bios/seabios.bin" },
filesystem: {
baseurl: "https://dtq9kvai1345o.cloudfront.net/8.1.0/fs/alpine-rootfs-flat",
basefs: "https://dtq9kvai1345o.cloudfront.net/8.1.0/fs/alpine-fs.json",
},
autostart: true,
bzimage_initrd_from_filesystem: true,
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable",
initial_state: { url: imgUrl },
disable_mouse: true,
disable_keyboard: true,
disable_speaker: true,
});

await new Promise(resolve => emulator.add_listener("emulator-ready", resolve));

const serialAdapter = new SerialAdapterXtermJS(document.getElementById('terminal-container'), emulator.bus);
serialAdapter.show();

document.getElementById("loadingContainer").style.display = "none";
document.getElementById("terminalWrapper").style.display = "flex";

if (emulator) {
resetInactivityTimer();
["mousemove", "keydown", "touchstart"].forEach(event => {
window.addEventListener(event, resetInactivityTimer);
});

serialAdapter.term.onKey(() => resetInactivityTimer()); // Typing
serialAdapter.term.onData(() => resetInactivityTimer()); // Sending data
serialAdapter.term.onCursorMove(() => resetInactivityTimer()); // Mouse activity
};
}

let inactivityTimeout;
const INACTIVITY_LIMIT = 60*1000*10 //inactivity limit is 10 minutes

function resetInactivityTimer() {
if (!emulator) {
console.warn("Emulator is not initialized yet.");
return;
}

clearTimeout(inactivityTimeout);

inactivityTimeout = setTimeout(() => {
if (emulator.is_running()) {
console.log("VM paused due to inactivity.");
emulator.stop();
}
}, INACTIVITY_LIMIT);

if (!emulator.is_running()) {
console.log("VM resumed");
emulator.run();
}
}

window.onload = function () {
const startButton = document.getElementById("startButton");
startButton.addEventListener("click", async () => {
document.getElementById("warningContainer").style.display = "none";
document.getElementById("loadingContainer").style.display = "block";
document.getElementById("progressText").innerText = "Preparing to load...";

const db = await openIndexedDB();

try {
const needsDownload = await checkIfUpdated();

if (needsDownload) {
downloadAndDecompressBinary(async (decompressedData) => {
await saveBinaryToCache(db, decompressedData);
loadEmulator(decompressedData);
});
} else {
const cachedBinary = await getCachedBinary(db);
if (cachedBinary) {
loadEmulator(cachedBinary);
} else {
downloadAndDecompressBinary(async (decompressedData) => {
await saveBinaryToCache(db, decompressedData);
loadEmulator(decompressedData);
});
}
}
} catch (error) {
console.error("Error loading binary: ", error);
document.getElementById("progressText").innerText = "Failed to load binary.";
}
});
};
</script>
{% endblock main_content %}