Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/template/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ pnpm-debug.log*
.idea

public/ruby.wasm
public/ruby.wasm.hash
10 changes: 10 additions & 0 deletions packages/template/bin/build-wasm
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ echo "Building WASM module..."
echo "WASM build completed successfully!"
echo "ruby.wasm is now available at ruby-wasm/dist/ruby.wasm"

# Generate hash of Gemfile.lock for cache versioning
echo "Generating Gemfile.lock hash..."
GEMFILE_HASH=$(sha256sum Gemfile.lock | cut -c1-16)
echo "Gemfile.lock hash: $GEMFILE_HASH"

# Copy the built ruby.wasm to the public directory
echo "Copying ruby.wasm to public directory..."
cp dist/ruby.wasm ../public/
echo "ruby.wasm copied to public directory."

# Create ruby.wasm.hash with the hash
echo "Creating ruby.wasm.hash..."
echo "$GEMFILE_HASH" > ../public/ruby.wasm.hash
echo "ruby.wasm.hash created with hash: $GEMFILE_HASH"
75 changes: 62 additions & 13 deletions packages/template/src/components/FileManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import tutorialStore from 'tutorialkit:store';

const VERSIONED_WASM_URL = `/ruby.wasm`;
const WASM_CACHE_FILE_NAME = `ruby.wasm`;
const GEMFILE_HASH_URL = `/ruby.wasm.hash`;
const WC_WASM_LOG_PATH = `/ruby.wasm.log.txt`;
const WC_WASM_PATH = `/ruby.wasm`;

Expand All @@ -15,6 +15,31 @@
const processedFiles = useRef(new Set<string>());
const wasmCached = useRef(false);

async function fetchGemfileHash(): Promise<string> {
try {
console.log(`Fetching Gemfile hash from ${GEMFILE_HASH_URL}...`);

const response = await fetch(GEMFILE_HASH_URL);

if (!response.ok) {
console.warn(`Failed to fetch ruby.wasm.hash: ${response.status}`);
return 'default';
}

const hash = (await response.text()).trim();
console.log(`Fetched Gemfile hash: ${hash}`);

return hash;
} catch (error) {
console.warn('Failed to fetch Gemfile hash, using default version:', error);
return 'default';
}
}

function getVersionedCacheFileName(gemfileLockHash: string): string {
return `ruby-${gemfileLockHash}.wasm`;
}

async function chmodx(wc: WebContainer, path: string) {
const process = await wc.spawn('chmod', ['+x', path]);

Expand All @@ -27,44 +52,63 @@
}
}

async function fetchCachedWasmFile(): Promise<Uint8Array | null> {
async function fetchCachedWasmFile(cacheFileName: string): Promise<Uint8Array | null> {
try {
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle(WASM_CACHE_FILE_NAME);
const fileHandle = await opfsRoot.getFileHandle(cacheFileName);
const file = await fileHandle.getFile();
console.log(`Found cached Ruby WASM: ${WASM_CACHE_FILE_NAME}`);
console.log(`Found cached Ruby WASM: ${cacheFileName}`);

return new Uint8Array(await file.arrayBuffer());
} catch {
return null;
}
}

async function persistWasmFile(wasmData: Uint8Array): Promise<void> {
async function persistWasmFile(wasmData: Uint8Array, cacheFileName: string): Promise<void> {
try {
const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle(WASM_CACHE_FILE_NAME, { create: true });
const fileHandle = await opfsRoot.getFileHandle(cacheFileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(wasmData);
await writable.close();
console.log(`Ruby WASM file ${WASM_CACHE_FILE_NAME} cached`);
console.log(`Ruby WASM file ${cacheFileName} cached`);
} catch (error) {
console.error('Failed to persist Ruby WASM:', error);
}
}

async function cacheWasmFile(wc: WebContainer): Promise<void> {
async function cleanupOldCacheFiles(currentCacheFileName: string): Promise<void> {
try {
const opfsRoot = await navigator.storage.getDirectory();

for await (const [name] of opfsRoot.entries()) {
if (
((name.startsWith('ruby-') && name.endsWith('.wasm')) || name === 'ruby.wasm') &&
name !== currentCacheFileName
) {
console.log(`Removing old cached Ruby WASM: ${name}`);
await opfsRoot.removeEntry(name);
}
}
} catch (error) {
console.warn('Failed to cleanup old cache files:', error);
}
}

async function cacheWasmFile(wc: WebContainer, cacheFileName: string): Promise<void> {
console.log(`Dowloading WASM file ${VERSIONED_WASM_URL}...`);

try {
const wasm = await fetch(VERSIONED_WASM_URL);
await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: downloaded');

const wasmData = new Uint8Array(await wasm.arrayBuffer());
await persistWasmFile(wasmData);
await persistWasmFile(wasmData, cacheFileName);
await cleanupOldCacheFiles(cacheFileName);
await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: cached');
await wc.fs.writeFile(WC_WASM_PATH, wasmData);
} catch (error) {
} catch {
await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: error');
}
}
Expand Down Expand Up @@ -93,23 +137,28 @@
if (!wasmCached.current) {
await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: init');

const cachedWasm = await fetchCachedWasmFile();
const gemfileLockHash = await fetchGemfileHash();
const cacheFileName = getVersionedCacheFileName(gemfileLockHash);
console.log(`Using cache file: ${cacheFileName} (hash: ${gemfileLockHash})`);

const cachedWasm = await fetchCachedWasmFile(cacheFileName);

if (cachedWasm) {
await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: load from cache');
await wc.fs.writeFile(WC_WASM_PATH, cachedWasm);
await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: done');
console.log(`Ruby WASM ${WASM_CACHE_FILE_NAME} loaded from cache`);
console.log(`Ruby WASM ${cacheFileName} loaded from cache`);
wasmCached.current = true;
} else {
await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: download');
await cacheWasmFile(wc);
await cacheWasmFile(wc, cacheFileName);
await wc.fs.writeFile(WC_WASM_LOG_PATH, 'status: done');
wasmCached.current = true;
}
}
})();

return () => {};

Check warning on line 161 in packages/template/src/components/FileManager.tsx

View workflow job for this annotation

GitHub Actions / Test (macos-latest, 20)

Unexpected empty arrow function

Check warning on line 161 in packages/template/src/components/FileManager.tsx

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 22)

Unexpected empty arrow function

Check warning on line 161 in packages/template/src/components/FileManager.tsx

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 18)

Unexpected empty arrow function

Check warning on line 161 in packages/template/src/components/FileManager.tsx

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest, 20)

Unexpected empty arrow function

Check warning on line 161 in packages/template/src/components/FileManager.tsx

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 20.13.1)

Unexpected empty arrow function
}, [lessonLoaded, files]);

return null;
Expand Down
Loading