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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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 @@ -36,6 +36,7 @@
<a role="menuitem" href="/blog/">Blog</a>
<a role="menuitem" href="/community/">Community</a>
<a role="menuitem" href="/participants/">Participants</a>
<a role="menuitem" href="/try-valkey/">Try Valkey</a>
</nav>
</div>
</div>
Expand Down
236 changes: 236 additions & 0 deletions templates/valkey-try-me.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
{% extends "fullwidth.html" %}
{%- block head -%}
<title>Try Valkey</title>

<!-- Scripts -->
<script src="https://download.valkey.io/try-me-valkey/vos/v86/libv86.js"></script>
<script src="https://download.valkey.io/try-me-valkey/vos/xterm/xterm.min.js"></script>
<script src="https://download.valkey.io/try-me-valkey/vos/pako/pako.min.js"></script>
<script src="https://download.valkey.io/try-me-valkey/vos/v86/serial_xterm.js"></script>
Comment on lines +6 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CDN usage looks good to me


<!-- Styles -->
<link rel="stylesheet" href="https://download.valkey.io/try-me-valkey/vos/xterm/xterm.css" />
<link rel="stylesheet" href="https://download.valkey.io/try-me-valkey/vos/valkey-try-me.css" />

{%- 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://download.valkey.io/try-me-valkey/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
Copy link
Preview

Copilot AI May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider adding more detailed comments explaining the structure and purpose of the IndexedDB caching logic to improve maintainability.

Suggested change
// Open or create IndexedDB
/**
* Opens an IndexedDB database named "binaryCacheDB" or creates it if it doesn't exist.
* This database is used to cache the binary data for the emulator, reducing the need
* to repeatedly download the binary file. The database contains a single object store
* named "cache" with a keyPath of "key".
*
* @returns {Promise<IDBDatabase>} A promise that resolves to the opened database instance.
*/

Copilot uses AI. Check for mistakes.

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);
Comment on lines +120 to +121
Copy link
Preview

Copilot AI May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding error handling around pako.ungzip to catch decompression errors and provide a user-friendly error message.

Suggested change
const decompressedData = pako.ungzip(new Uint8Array(xhr.response));
callback(decompressedData);
try {
const decompressedData = pako.ungzip(new Uint8Array(xhr.response));
callback(decompressedData);
} catch (error) {
console.error("Decompression error:", error);
document.getElementById("progressText").innerText = "Decompression failed. Please try again.";
}

Copilot uses AI. Check for mistakes.

}
};

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://download.valkey.io/try-me-valkey/vos/v86/v86.wasm",
memory_size: 512 * 1024 * 1024,
bios: { url: "https://download.valkey.io/try-me-valkey/vos/v86/bios/seabios.bin" },
filesystem: {
baseurl: "https://download.valkey.io/try-me-valkey/8.1.0/fs/alpine-rootfs-flat",
basefs: "https://download.valkey.io/try-me-valkey/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

Comment on lines +167 to +179
Copy link
Preview

Copilot AI May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider debouncing the calls to resetInactivityTimer to reduce potential performance overhead during rapid key events.

Suggested change
["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
const debouncedResetInactivityTimer = debounce(resetInactivityTimer, 200);
["mousemove", "keydown", "touchstart"].forEach(event => {
window.addEventListener(event, debouncedResetInactivityTimer);
});
serialAdapter.term.onKey(() => debouncedResetInactivityTimer()); // Typing
serialAdapter.term.onData(() => debouncedResetInactivityTimer()); // Sending data
serialAdapter.term.onCursorMove(() => debouncedResetInactivityTimer()); // Mouse activity
};
}
let inactivityTimeout;
const INACTIVITY_LIMIT = 60*1000*10 //inactivity limit is 10 minutes
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}

Copilot uses AI. Check for mistakes.

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 %}