Skip to content

Commit 04f4f09

Browse files
zarkash-awsphiro56madolson
authored
added Try Valkey support (#255)
### General Description This PR implements a browser-based environment that allows users to interact with Valkey-CLI commands without needing local installation. This feature was initially introduced in [Issue #1412](valkey-io/valkey#1412), and it aims to provide a lightweight, easy-to-access platform for users to explore Valkey’s functionalities directly in the browser. ### Implementation The solution uses a virtualized environment powered by [v86](https://github.com/copy/v86), a tool that emulates an x86-compatible CPU and hardware. The machine code for the environment is translated into WebAssembly modules at runtime, allowing it to run entirely within the browser. * VM Configuration: When the page is loaded, the VM loads a preconfigured Alpine Linux image that already has the Valkey server and CLI installed. This configuration ensures users can immediately interact with the Valkey CLI upon starting the web-based environment. * Current Version: The current version of Valkey in the preconfigured image is 8.1.0. However, binaries for earlier versions (7.2.8 and 8.0.1) are also available in the S3 bucket. We can decide whether to set a single version or implement a dropdown menu that allows users to choose the version they want to interact with. ### Serving of Binary Files The virtual machine used for this setup relies on two main binary file components: 1. Binary files for the VM’s filesystem. 2. A binary file containing a preloaded state, which allows for seamless page loading. These binaries are served from an S3 bucket (currently set as a test bucket) via CloudFront CDN. * CORS Policy: To allow the browser to access these binary files, we need to configure the appropriate CORS policy. Specifically, the “Access-Control-Allow-Origin” header must be set correctly. This CORS policy needs to be configured both for the S3 bucket and the CloudFront distribution. * We can set this policy only for the Try-Valkey files directory if we prefer not to apply it to the entire bucket. * Additionally, we can restrict access to these files by specifying that only the Valkey website domain can use them. More details can be found [here](https://repost.aws/knowledge-center/no-access-control-allow-origin-error). ### New Image Creation Process Currently, the creation of new images for future Valkey versions will be done manually. However, we plan to automate this process in the future to simplify updates as new versions of Valkey are released. I have also created a [repository](https://github.com/valkey-io/valkey-try-me) for documentation and for creating new images for Try Valkey. ### Screenshots new "Try Valkey" tab <img width="1510" alt="Screenshot 2025-06-03 at 11 48 36" src="https://github.com/user-attachments/assets/d881c7b6-6156-4e85-aa45-673beb76528b" /> data warning <img width="1510" alt="Screenshot 2025-06-03 at 11 48 47" src="https://github.com/user-attachments/assets/d9c4c75c-90de-4c83-a3cd-a85bab2a648b" /> after pressing "load emulator" <img width="1510" alt="Screenshot 2025-06-03 at 11 49 00" src="https://github.com/user-attachments/assets/ea2f98c5-239d-462f-a2a7-3dd265ad66e5" /> --------- Signed-off-by: Shai Zarka <[email protected]> Signed-off-by: Daniel Phillips <[email protected]> Signed-off-by: Madelyn Olson <[email protected]> Co-authored-by: Daniel Phillips <[email protected]> Co-authored-by: Madelyn Olson <[email protected]>
1 parent f364852 commit 04f4f09

File tree

3 files changed

+240
-0
lines changed

3 files changed

+240
-0
lines changed

content/try-valkey/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
+++
2+
title = "Try Valkey"
3+
template = "valkey-try-me.html"
4+
+++
5+

templates/default.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
<a role="menuitem" href="/blog/">Blog</a>
3737
<a role="menuitem" href="/community/">Community</a>
3838
<a role="menuitem" href="/participants/">Participants</a>
39+
<a role="menuitem" href="/try-valkey/">Try Valkey</a>
40+
<a role="menuitem" href="/community/">Community</a>
41+
<a role="menuitem" href="/participants/">Participants</a>
3942
</nav>
4043
</div>
4144
</div>

templates/valkey-try-me.html

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
{% extends "fullwidth.html" %}
2+
{% block main_content %}
3+
<!-- Scripts -->
4+
<script src="https://download.valkey.io/try-me-valkey/vos/v86/libv86.js"></script>
5+
<script src="https://download.valkey.io/try-me-valkey/vos/xterm/xterm.min.js"></script>
6+
<script src="https://download.valkey.io/try-me-valkey/vos/pako/pako.min.js"></script>
7+
<script src="https://download.valkey.io/try-me-valkey/vos/v86/serial_xterm.js"></script>
8+
9+
<!-- Styles -->
10+
<link rel="stylesheet" href="https://download.valkey.io/try-me-valkey/vos/xterm/xterm.css" />
11+
<link rel="stylesheet" href="https://download.valkey.io/try-me-valkey/vos/valkey-try-me.css" />
12+
13+
<!-- Body -->
14+
<title>Try Valkey</title>
15+
<p>This is an in-browser Valkey server and CLI that runs directly within your browser using a <a href="https://github.com/copy/v86">V86</a> emulator, requiring no external installations. </p>
16+
<p>Try it out below:</p>
17+
<div id="terminalWrapper" class="container" style="display: none;">
18+
<div id="terminal-container"></div>
19+
</div>
20+
<!-- Warning Section -->
21+
<div id="warningContainer" style="text-align: center; margin-top: 20px;">
22+
<button id="startButton" style="padding: 10px 20px; font-size: 18px; margin-top: 10px; cursor: pointer;">Load Emulator</button>
23+
<p>This emulator will download approximately 50MB of data.</p>
24+
</div>
25+
<!-- Loading Section (Hidden at first) -->
26+
<div id="loadingContainer" style="display: none;">
27+
<p id="progressText">Preparing to load...</p>
28+
<progress id="progressBar" value="0" max="100"></progress>
29+
</div>
30+
31+
<script>
32+
"use strict";
33+
const FILE_URL = "https://download.valkey.io/try-me-valkey/8.1.0/states/state.bin.gz"; // Path to the .gz file
34+
const CACHE_KEY = "valkey_binary_cache";
35+
const LAST_MODIFIED_KEY = "valkey_last_modified";
36+
let emulator;
37+
38+
// Open or create IndexedDB
39+
async function openIndexedDB() {
40+
return new Promise((resolve, reject) => {
41+
const request = indexedDB.open("binaryCacheDB", 1);
42+
43+
request.onerror = () => reject("Error opening IndexedDB");
44+
request.onsuccess = () => resolve(request.result);
45+
46+
request.onupgradeneeded = (event) => {
47+
const db = event.target.result;
48+
db.createObjectStore("cache", { keyPath: "key" });
49+
};
50+
});
51+
}
52+
53+
// Retrieve binary from cache
54+
async function getCachedBinary(db) {
55+
return new Promise((resolve, reject) => {
56+
const transaction = db.transaction(["cache"], "readonly");
57+
const objectStore = transaction.objectStore("cache");
58+
const request = objectStore.get(CACHE_KEY);
59+
60+
request.onerror = () => reject("Error retrieving cached binary");
61+
request.onsuccess = () => resolve(request.result ? request.result.data : null);
62+
});
63+
}
64+
65+
// Save binary to cache
66+
async function saveBinaryToCache(db, data) {
67+
return new Promise((resolve, reject) => {
68+
const transaction = db.transaction(["cache"], "readwrite");
69+
const objectStore = transaction.objectStore("cache");
70+
const request = objectStore.put({ key: CACHE_KEY, data });
71+
72+
request.onerror = () => reject("Error saving binary to cache");
73+
request.onsuccess = () => resolve();
74+
});
75+
}
76+
77+
// Check if binary is updated
78+
async function checkIfUpdated() {
79+
return new Promise((resolve, reject) => {
80+
const xhr = new XMLHttpRequest();
81+
xhr.open("HEAD", FILE_URL, true);
82+
83+
xhr.onload = () => {
84+
const serverLastModified = xhr.getResponseHeader("Last-Modified");
85+
const cachedLastModified = localStorage.getItem(LAST_MODIFIED_KEY);
86+
87+
if (!serverLastModified || serverLastModified !== cachedLastModified) {
88+
localStorage.setItem(LAST_MODIFIED_KEY, serverLastModified);
89+
resolve(true);
90+
} else {
91+
resolve(false);
92+
}
93+
};
94+
95+
xhr.onerror = () => reject("Error checking file version");
96+
xhr.send();
97+
});
98+
}
99+
100+
// Download and decompress binary
101+
function downloadAndDecompressBinary(callback) {
102+
const xhr = new XMLHttpRequest();
103+
xhr.open("GET", FILE_URL, true);
104+
xhr.responseType = "arraybuffer";
105+
106+
xhr.onprogress = (event) => {
107+
if (event.lengthComputable) {
108+
const percentComplete = (event.loaded / event.total) * 100;
109+
document.getElementById("progressBar").value = percentComplete;
110+
}
111+
};
112+
113+
xhr.onload = () => {
114+
if (xhr.status === 200) {
115+
document.getElementById("progressText").innerText = "Decompressing image...";
116+
const decompressedData = pako.ungzip(new Uint8Array(xhr.response));
117+
callback(decompressedData);
118+
}
119+
};
120+
121+
xhr.onerror = () => {
122+
document.getElementById("progressText").innerText = "Download failed!";
123+
};
124+
125+
xhr.send();
126+
}
127+
128+
async function loadEmulator(decompressedData) {
129+
const progressText = document.getElementById("progressText");
130+
131+
const blob = new Blob([decompressedData], { type: "application/octet-stream" });
132+
const imgUrl = URL.createObjectURL(blob);
133+
134+
progressText.innerText = "Starting emulator...";
135+
136+
emulator = new V86({
137+
wasm_path: "https://download.valkey.io/try-me-valkey/vos/v86/v86.wasm",
138+
memory_size: 512 * 1024 * 1024,
139+
bios: { url: "https://download.valkey.io/try-me-valkey/vos/v86/bios/seabios.bin" },
140+
filesystem: {
141+
baseurl: "https://download.valkey.io/try-me-valkey/8.1.0/fs/alpine-rootfs-flat",
142+
basefs: "https://download.valkey.io/try-me-valkey/8.1.0/fs/alpine-fs.json",
143+
},
144+
autostart: true,
145+
bzimage_initrd_from_filesystem: true,
146+
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable",
147+
initial_state: { url: imgUrl },
148+
disable_mouse: true,
149+
disable_keyboard: true,
150+
disable_speaker: true,
151+
});
152+
153+
await new Promise(resolve => emulator.add_listener("emulator-ready", resolve));
154+
155+
const serialAdapter = new SerialAdapterXtermJS(document.getElementById('terminal-container'), emulator.bus);
156+
serialAdapter.show();
157+
158+
document.getElementById("loadingContainer").style.display = "none";
159+
document.getElementById("terminalWrapper").style.display = "flex";
160+
161+
if (emulator) {
162+
resetInactivityTimer();
163+
["mousemove", "keydown", "touchstart"].forEach(event => {
164+
window.addEventListener(event, resetInactivityTimer);
165+
});
166+
167+
serialAdapter.term.onKey(() => resetInactivityTimer()); // Typing
168+
serialAdapter.term.onData(() => resetInactivityTimer()); // Sending data
169+
serialAdapter.term.onCursorMove(() => resetInactivityTimer()); // Mouse activity
170+
};
171+
}
172+
173+
let inactivityTimeout;
174+
const INACTIVITY_LIMIT = 60*1000*10 //inactivity limit is 10 minutes
175+
176+
function resetInactivityTimer() {
177+
if (!emulator) {
178+
console.warn("Emulator is not initialized yet.");
179+
return;
180+
}
181+
182+
clearTimeout(inactivityTimeout);
183+
184+
inactivityTimeout = setTimeout(() => {
185+
if (emulator.is_running()) {
186+
console.log("VM paused due to inactivity.");
187+
emulator.stop();
188+
}
189+
}, INACTIVITY_LIMIT);
190+
191+
if (!emulator.is_running()) {
192+
console.log("VM resumed");
193+
emulator.run();
194+
}
195+
}
196+
197+
window.onload = function () {
198+
const startButton = document.getElementById("startButton");
199+
startButton.addEventListener("click", async () => {
200+
document.getElementById("warningContainer").style.display = "none";
201+
document.getElementById("loadingContainer").style.display = "block";
202+
document.getElementById("progressText").innerText = "Preparing to load...";
203+
204+
const db = await openIndexedDB();
205+
206+
try {
207+
const needsDownload = await checkIfUpdated();
208+
209+
if (needsDownload) {
210+
downloadAndDecompressBinary(async (decompressedData) => {
211+
await saveBinaryToCache(db, decompressedData);
212+
loadEmulator(decompressedData);
213+
});
214+
} else {
215+
const cachedBinary = await getCachedBinary(db);
216+
if (cachedBinary) {
217+
loadEmulator(cachedBinary);
218+
} else {
219+
downloadAndDecompressBinary(async (decompressedData) => {
220+
await saveBinaryToCache(db, decompressedData);
221+
loadEmulator(decompressedData);
222+
});
223+
}
224+
}
225+
} catch (error) {
226+
console.error("Error loading binary: ", error);
227+
document.getElementById("progressText").innerText = "Failed to load binary.";
228+
}
229+
});
230+
};
231+
</script>
232+
{% endblock main_content %}

0 commit comments

Comments
 (0)