Skip to content
Closed
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
15 changes: 14 additions & 1 deletion tinytorch/quarto/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@ project:
# install.sh and community/ moved here from the retired tinytorch/site/
# (Jupyter Book) tree so this quarto tree is the only source for
# anything published under /tinytorch/.
# - manifest.webmanifest / sw.js / assets/images/icons/**
# PWA installability + offline support. The
# manifest link and service worker registration
# are injected by assets/scripts/pwa.html, so
# Quarto never sees them referenced statically.
resources:
- assets/downloads/**
- install.sh
- community/**
- manifest.webmanifest
- sw.js
- assets/images/icons/**

website:
title: "TinyTorch"
Expand Down Expand Up @@ -269,14 +277,19 @@ format:
include-in-header:
- file: ../../shared/config/site-head.html
- text: |
<link rel="apple-touch-icon" href="/assets/images/logos/logo-tinytorch.png">
<!-- Leading-/ hrefs are project-root-relative: Quarto rewrites
them per page depth (../ on nested pages), so they resolve
correctly under mlsysbook.ai/tinytorch/ and local previews. -->
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/icons/apple-touch-icon.png">
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#D4740C">
<!-- Release identity (rendered into the footer pill below).
The publish workflow drops release-manifest.json at
/tinytorch/release-manifest.json before deploy; preview
and local renders silently no-op. -->
<meta name="release-manifest" content="/tinytorch/release-manifest.json">
include-after-body:
- file: assets/scripts/pwa.html
- file: assets/scripts/subscribe-modal.js
- file: assets/scripts/ml-timeline.js
- file: assets/scripts/sidebar-subtitle.html
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tinytorch/quarto/assets/images/icons/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tinytorch/quarto/assets/images/icons/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions tinytorch/quarto/assets/scripts/pwa.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script>
/*
* PWA bootstrap — registers the service worker (sw.js at the site root, so
* its scope covers the whole TinyTorch site). The manifest link lives in
* include-in-header (_quarto.yml); only the worker needs JS because
* registration takes a URL resolved against the page, and pages render at
* several directory depths. The site deploys under /tinytorch/ on
* mlsysbook.ai but local previews serve the build at the server root, so
* the base path is derived at runtime to handle both.
*/
(function () {
'use strict';
var secureContext = window.location.protocol === 'https:' ||
window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
if (!('serviceWorker' in navigator) || !secureContext) return;

var match = window.location.pathname.match(/^(.*\/tinytorch\/)/);
var base = match ? match[1] : '/';
window.addEventListener('load', function () {
navigator.serviceWorker.register(base + 'sw.js').catch(function (err) {
// Expected on previews that serve the build under a different root;
// the site works identically without the worker.
console.warn('TinyTorch PWA: service worker registration failed:', err);
});
});
})();
</script>
38 changes: 38 additions & 0 deletions tinytorch/quarto/manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "TinyTorch — Build Your Own ML Framework",
"short_name": "TinyTorch",
"description": "An interactive course for building machine learning systems from the ground up. Learn by implementing your own PyTorch-style framework.",
"id": "./",
"start_url": "./",
"scope": "./",
"display": "standalone",
"orientation": "any",
"background_color": "#ffffff",
"theme_color": "#D4740C",
"icons": [
{
"src": "assets/images/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "assets/images/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "assets/images/icons/icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "assets/images/icons/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
155 changes: 155 additions & 0 deletions tinytorch/quarto/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* TinyTorch service worker — offline support for the PWA.
*
* Strategy:
* - Navigations (HTML): network-first. The site deploys frequently, so
* pages must never go stale; the cache is only a fallback for offline
* revisits, with a branded offline page as the last resort.
* - Same-origin static assets (css/js/images/fonts): stale-while-revalidate.
* One deploy of staleness is acceptable for assets and keeps repeat
* visits fast.
* - Cross-origin requests (Google Fonts, cdnjs) are left to the browser —
* caching opaque responses inflates storage quota unpredictably.
*
* Bump CACHE_VERSION when changing caching behavior; old caches are dropped
* on activate.
*/

'use strict';

const CACHE_VERSION = 'v1';
const PAGE_CACHE = `tinytorch-pages-${CACHE_VERSION}`;
const ASSET_CACHE = `tinytorch-assets-${CACHE_VERSION}`;
const ASSET_CACHE_LIMIT = 200;

// Everything the offline fallback needs is inlined below, so install only
// warms the cache with the manifest and install-prompt icons.
const PRECACHE_URLS = [
'./manifest.webmanifest',
'./assets/images/icons/icon-192.png',
'./assets/images/icons/icon-512.png',
];

const OFFLINE_HTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Offline — TinyTorch</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex; align-items: center; justify-content: center;
min-height: 100vh; margin: 0; background: #fff; color: #333;
text-align: center; padding: 1rem;
}
@media (prefers-color-scheme: dark) {
body { background: #1a1a1a; color: #ddd; }
}
.flame { font-size: 4rem; }
h1 { margin: 0.5rem 0; font-size: 1.5rem; }
p { color: #888; max-width: 28rem; }
button {
margin-top: 1rem; padding: 0.5rem 1.5rem; font-size: 1rem;
background: #D4740C; color: #fff; border: none; border-radius: 6px;
cursor: pointer;
}
</style>
</head>
<body>
<main>
<div class="flame">🔥</div>
<h1>You're offline</h1>
<p>This TinyTorch page isn't cached yet. Pages you've visited before are
available offline — reconnect to load this one.</p>
<button onclick="location.reload()">Try again</button>
</main>
</body>
</html>`;

self.addEventListener('install', (event) => {
event.waitUntil(
caches
.open(ASSET_CACHE)
.then((cache) => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
);
});

self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
const names = await caches.keys();
await Promise.all(
names
.filter((n) => n.startsWith('tinytorch-') && n !== PAGE_CACHE && n !== ASSET_CACHE)
.map((n) => caches.delete(n))
);
if (self.registration.navigationPreload) {
await self.registration.navigationPreload.enable();
}
await self.clients.claim();
})()
);
});

async function trimCache(cacheName, limit) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length <= limit) return;
// Cache keys are ordered oldest-first; drop from the front.
await Promise.all(keys.slice(0, keys.length - limit).map((k) => cache.delete(k)));
}

async function networkFirstPage(event) {
const cache = await caches.open(PAGE_CACHE);
try {
const response = (await event.preloadResponse) || (await fetch(event.request));
if (response && response.ok) {
cache.put(event.request, response.clone());
}
return response;
} catch (err) {
const cached = await cache.match(event.request);
if (cached) return cached;
return new Response(OFFLINE_HTML, {
status: 503,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
}

async function staleWhileRevalidate(request) {
const cache = await caches.open(ASSET_CACHE);
const cached = await cache.match(request);
const network = fetch(request)
.then((response) => {
if (response && response.ok) {
cache.put(request, response.clone());
trimCache(ASSET_CACHE, ASSET_CACHE_LIMIT);
}
return response;
})
.catch(() => undefined);
return cached || network.then((r) => r || Response.error());
}

self.addEventListener('fetch', (event) => {
const request = event.request;
if (request.method !== 'GET') return;

const url = new URL(request.url);
if (url.origin !== self.location.origin) return;

if (request.mode === 'navigate') {
event.respondWith(networkFirstPage(event));
return;
}

const dest = request.destination;
if (dest === 'style' || dest === 'script' || dest === 'image' || dest === 'font') {
event.respondWith(staleWhileRevalidate(request));
}
// Everything else (search.json, release-manifest.json, XHR/fetch data)
// goes straight to the network so dynamic data is never served stale.
});
Loading