<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: webworkers</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/webworkers.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2024-10-24T20:22:52+00:00</updated><author><name>Simon Willison</name></author><entry><title>Notes on the new Claude analysis JavaScript code execution tool</title><link href="https://simonwillison.net/2024/Oct/24/claude-analysis-tool/#atom-tag" rel="alternate"/><published>2024-10-24T20:22:52+00:00</published><updated>2024-10-24T20:22:52+00:00</updated><id>https://simonwillison.net/2024/Oct/24/claude-analysis-tool/#atom-tag</id><summary type="html">
    &lt;p&gt;Anthropic &lt;a href="https://www.anthropic.com/news/analysis-tool"&gt;released a new feature&lt;/a&gt; for their &lt;a href="http://claude.ai/"&gt;Claude.ai&lt;/a&gt; consumer-facing chat bot interface today which they're calling "the analysis tool".&lt;/p&gt;
&lt;p&gt;It's their answer to OpenAI's &lt;a href="https://simonwillison.net/tags/code-interpreter/"&gt;ChatGPT Code Interpreter&lt;/a&gt; mode: Claude can now chose to solve models by writing some code, executing that code and then continuing the conversation using the results from that execution.&lt;/p&gt;
&lt;p&gt;You can enable the new feature on the &lt;a href="https://claude.ai/new?fp=1"&gt;Claude feature flags page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I tried uploading a &lt;code&gt;uv.lock&lt;/code&gt; dependency file (which uses TOML syntax) and telling it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Write a parser for this file format and show me a visualization of what's in it&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It gave me this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Claude screenshot. I've uploaded a uv.lock file and prompted &amp;quot;Write a parser for this file format and show me a visualization of what's in it&amp;quot; Claude: I'll help create a parser and visualization for this lockfile format. It appears to be similar to a TOML-based lock file used in Python package management. Let me analyze the structure and create a visualization. Visible code: const fileContent = await window.fs.readFile('uv.lock', { encoding: 'utf8' }); function parseLockFile(content) ... On the right, an SVG visualization showing packages in a circle with lines between them, and an anyio package description" src="https://static.simonwillison.net/static/2024/analysis-uv-lock.jpg" style="max-width: 100%" /&gt;&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/b25198899f92bdd7f15830567a07e319"&gt;that chat transcript&lt;/a&gt; and &lt;a href="https://static.simonwillison.net/static/2024/uv-lock-vis/index.html"&gt;the resulting artifact&lt;/a&gt;. I upgraded my &lt;a href="https://observablehq.com/@simonw/convert-claude-json-to-markdown"&gt;Claude transcript export tool&lt;/a&gt; to handle the new feature, and hacked around with &lt;a href="https://simonwillison.net/2024/Oct/23/claude-artifact-runner/"&gt;Claude Artifact Runner&lt;/a&gt; (manually editing the source to replace &lt;code&gt;fs.readFile()&lt;/code&gt; with a constant) to build the React artifact separately.&lt;/p&gt;
&lt;p&gt;ChatGPT Code Interpreter (and the under-documented &lt;a href="https://ai.google.dev/gemini-api/docs/code-execution"&gt;Google Gemini equivalent&lt;/a&gt;) both work the same way: they write Python code which then runs in a secure sandbox on OpenAI or Google's servers.&lt;/p&gt;
&lt;p&gt;Claude does things differently. It uses JavaScript rather than Python, and it executes that JavaScript directly in your browser - in a locked down &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers"&gt;Web Worker&lt;/a&gt; that communicates back to the main page by intercepting messages sent to &lt;code&gt;console.log()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It's implemented as a tool called &lt;code&gt;repl&lt;/code&gt;, and you can prompt Claude like this to reveal some of the custom instructions that are used to drive it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Show me the full description of the repl function&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/348b4ef2289cb5b1dee9aea9863bbc01"&gt;what I managed to extract&lt;/a&gt; using that. This is how those instructions start:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is the analysis tool?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The analysis tool &lt;em&gt;is&lt;/em&gt; a JavaScript REPL. You can use it just like you would use a REPL. But from here on out, we will call it the analysis tool.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When to use the analysis tool&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use the analysis tool for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complex math problems that require a high level of accuracy and cannot easily be done with "mental math"&lt;ul&gt;
&lt;li&gt;To give you the idea, 4-digit multiplication is within your capabilities, 5-digit multiplication is borderline, and 6-digit multiplication would necessitate using the tool.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Analyzing user-uploaded files, particularly when these files are large and contain more data than you could reasonably handle within the span of your output limit (which is around 6,000 words).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The analysis tool has access to a &lt;code&gt;fs.readFile()&lt;/code&gt; function that can read data from files you have shared with your Claude conversation. It also has access to the &lt;a href="https://lodash.com/"&gt;Lodash&lt;/a&gt; utility library and &lt;a href="https://www.papaparse.com/"&gt;Papa Parse&lt;/a&gt; for parsing CSV content. The instructions say:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can import available libraries such as lodash and papaparse in the analysis tool. However, note that the analysis tool is NOT a Node.js environment. Imports in the analysis tool work the same way they do in React. Instead of trying to get an import from the window, import using React style import syntax. E.g., you can write &lt;code&gt;import Papa from 'papaparse';&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'm not sure why it says "libraries such as ..." there when as far as I can tell Lodash and papaparse are the &lt;em&gt;only&lt;/em&gt; libraries it can load - unlike Claude Artifacts it can't pull in other packages from its CDN.&lt;/p&gt;
&lt;p id="apologize"&gt;At one point in the instructions the Claude engineers &lt;em&gt;apologize&lt;/em&gt; to the LLM! Emphasis mine:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;When using the analysis tool, you &lt;em&gt;must&lt;/em&gt; use the correct antml syntax provided in the tool. Pay attention to the prefix. To reiterate, anytime you use the analysis tool, you &lt;em&gt;must&lt;/em&gt; use antml syntax. Please note that this is similar but not identical to the antArtifact syntax which is used for Artifacts; &lt;strong&gt;sorry for the ambiguity&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The interaction between the analysis tool and Claude Artifacts is somewhat confusing. Here's the relevant piece of the tool instructions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Code that you write in the analysis tool is &lt;em&gt;NOT&lt;/em&gt; in a shared environment with the Artifact. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To reuse code from the analysis tool in an Artifact, you must rewrite the code in its entirety in the Artifact.&lt;/li&gt;
&lt;li&gt;You cannot add an object to the &lt;code&gt;window&lt;/code&gt; and expect to be able to read it in the Artifact. Instead, use the &lt;code&gt;window.fs.readFile&lt;/code&gt; api to read the CSV in the Artifact after first reading it in the analysis tool.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A further limitation of the analysis tool is that any files you upload to it are currently added to the Claude context. This means there's a size limit, and also means that only text formats work right now - you can't upload a binary (as I found when I tried uploading &lt;a href="https://github.com/sqlite/sqlite-wasm/tree/main/sqlite-wasm/jswasm"&gt;sqlite.wasm&lt;/a&gt; to see if I could get it to use SQLite).&lt;/p&gt;
&lt;p&gt;Anthropic's Alex Albert says &lt;a href="https://twitter.com/alexalbert__/status/1849501507005149515"&gt;this will change in the future&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Yep currently the data is within the context window - we're working on moving it out.&lt;/p&gt;
&lt;/blockquote&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-engineering"&gt;prompt-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/code-interpreter"&gt;code-interpreter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-albert"&gt;alex-albert&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-tool-use"&gt;llm-tool-use&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-artifacts"&gt;claude-artifacts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-to-app"&gt;prompt-to-app&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="javascript"/><category term="webworkers"/><category term="ai"/><category term="prompt-engineering"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="anthropic"/><category term="claude"/><category term="code-interpreter"/><category term="alex-albert"/><category term="llm-tool-use"/><category term="claude-artifacts"/><category term="coding-agents"/><category term="prompt-to-app"/></entry><entry><title>Instant colour fill with HTML Canvas</title><link href="https://simonwillison.net/2023/May/24/instant-colour-fill-with-html-canvas/#atom-tag" rel="alternate"/><published>2023-05-24T01:27:00+00:00</published><updated>2023-05-24T01:27:00+00:00</updated><id>https://simonwillison.net/2023/May/24/instant-colour-fill-with-html-canvas/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://shaneosullivan.wordpress.com/2023/05/23/instant-colour-fill-with-html-canvas/"&gt;Instant colour fill with HTML Canvas&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Shane O'Sullivan describes how to implement instant colour fill using HTML Canvas and some really clever tricks with Web Workers. A new technique to me is passing a &lt;code&gt;canvas.getImageData()&lt;/code&gt; object to a Web Worker via &lt;code&gt;worker.postMessage({action: "process", buffer: imageData.data.buffer}, [imageData.data.buffer])&lt;/code&gt; where that second argument is a list of objects to "transfer ownership of" - then the worker can create a new &lt;code&gt;ImageData()&lt;/code&gt;, populate it and transfer ownership of that back to the parent window.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=36049386"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/canvas"&gt;canvas&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;&lt;/p&gt;



</summary><category term="canvas"/><category term="javascript"/><category term="webworkers"/></entry><entry><title>Building a Signal Analyzer with Modern Web Tech</title><link href="https://simonwillison.net/2023/May/21/signal-analyzer/#atom-tag" rel="alternate"/><published>2023-05-21T21:35:19+00:00</published><updated>2023-05-21T21:35:19+00:00</updated><id>https://simonwillison.net/2023/May/21/signal-analyzer/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://cprimozic.net/blog/building-a-signal-analyzer-with-modern-web-tech/"&gt;Building a Signal Analyzer with Modern Web Tech&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Casey Primozic’s detailed write-up of his project to build a spectrogram and oscilloscope using cutting-edge modern web technology: Web Workers, Web Audio, SharedArrayBuffer, Atomics.waitAsync, OffscreenCanvas, WebAssembly SIMD and more. His conclusion: “Web developers now have all the tools they need to build native-or-better quality apps on the web.”

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=36010600"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="webworkers"/><category term="webassembly"/></entry><entry><title>Datasette Lite: a server-side Python web application running in a browser</title><link href="https://simonwillison.net/2022/May/4/datasette-lite/#atom-tag" rel="alternate"/><published>2022-05-04T15:16:49+00:00</published><updated>2022-05-04T15:16:49+00:00</updated><id>https://simonwillison.net/2022/May/4/datasette-lite/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://github.com/simonw/datasette-lite"&gt;Datasette Lite&lt;/a&gt; is a new way to run &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt;: entirely in a browser, taking advantage of the incredible &lt;a href="https://pyodide.org/"&gt;Pyodide&lt;/a&gt; project which provides Python compiled to WebAssembly plus a whole suite of useful extras.&lt;/p&gt;
&lt;p&gt;You can try it out here:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://lite.datasette.io/"&gt;https://lite.datasette.io/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/datasette-lite.jpg" alt="A screenshot of the pypi_packages database table running in Google Chrome in a page with the URL of lite.datasette.io/#/content/pypi_packages?_facet=author" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Update 20th June 2022&lt;/strong&gt;: Datasette Lite can now load CSV files directly by URL, see &lt;a href="https://simonwillison.net/2022/Jun/20/datasette-lite-csvs/"&gt;Joining CSV files in your browser using Datasette Lite&lt;/a&gt; for details.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Update 17th August 2022&lt;/strong&gt;: It can now &lt;a href="https://simonwillison.net/2022/Aug/17/datasette-lite-plugins/"&gt;load Datasette plugins as well&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The initial example loads two databases - the classic &lt;a href="https://latest.datasette.io/fixtures"&gt;fixtures.db&lt;/a&gt; used by the Datasette test suite, and the &lt;a href="https://datasette.io/content"&gt;content.db&lt;/a&gt; database that powers the official &lt;a href="https://datasette.io/"&gt;datasette.io&lt;/a&gt; website (described in some detail in &lt;a href="https://simonwillison.net/2021/Jul/28/baked-data/"&gt;my post about Baked Data&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;You can instead use the "Load database by URL to a SQLite DB" button to paste in a URL to your own database. That file will need to be served with CORS headers that allow it to be fetched by the website (&lt;a href="https://github.com/simonw/datasette-lite/#opening-other-databases"&gt;see README&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Try this URL, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://congress-legislators.datasettes.com/legislators.db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can &lt;a href="https://lite.datasette.io/?url=https%3A%2F%2Fcongress-legislators.datasettes.com%2Flegislators.db"&gt;follow this link&lt;/a&gt; to open that database in Datasette Lite.&lt;/p&gt;
&lt;p&gt;Datasette Lite supports almost all of Datasette's regular functionality: you can view tables, apply facets, run your own custom SQL results and export the results as CSV or JSON.&lt;/p&gt;
&lt;p&gt;It's basically the full Datasette experience, except it's running entirely in your browser with no server (other than the static file hosting provided here by GitHub Pages) required.&lt;/p&gt;
&lt;p&gt;I’m pretty stunned that this is possible now.&lt;/p&gt;
&lt;p&gt;I had to make some small changes to Datasette to get this to work, detailed below, but really nothing extravagant - the demo is running the exact same Python code as the regular server-side Datasette application, just inside a web worker process in a browser rather than on a server.&lt;/p&gt;
&lt;p&gt;The implementation is pretty small - around 300 lines of JavaScript. You can see the code in the &lt;a href="https://github.com/simonw/datasette-lite"&gt;simonw/datasette-lite&lt;/a&gt; repository - in two files, &lt;a href="https://github.com/simonw/datasette-lite/blob/6ae4cacf140f0c7c6eafa8cf0f92a2dae44425ff/index.html"&gt;index.html&lt;/a&gt; and &lt;a href="https://github.com/simonw/datasette-lite/blob/main/webworker.js"&gt;webworker.js&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;Why build this?&lt;/h4&gt;
&lt;p&gt;I built this because I want as many people as possible to be able to use my software.&lt;/p&gt;
&lt;p&gt;I've invested a ton of effort in reducing the friction to getting started with Datasette. I've &lt;a href="https://docs.datasette.io/en/stable/installation.html"&gt;documented the install process&lt;/a&gt;, I've &lt;a href="https://formulae.brew.sh/formula/datasette"&gt;packaged it for Homebrew&lt;/a&gt;, I've written guides to &lt;a href="https://docs.datasette.io/en/stable/getting_started.html#try-datasette-without-installing-anything-using-glitch"&gt;running it on Glitch&lt;/a&gt;, I've built tools to help deploy it to &lt;a href="https://docs.datasette.io/en/stable/publish.html#publishing-to-heroku"&gt;Heroku&lt;/a&gt;, &lt;a href="https://docs.datasette.io/en/stable/publish.html#publishing-to-google-cloud-run"&gt;Cloud Run&lt;/a&gt;, &lt;a href="https://docs.datasette.io/en/stable/publish.html#publishing-to-vercel"&gt;Vercel&lt;/a&gt; and &lt;a href="https://simonwillison.net/2022/Feb/15/fly-volumes/"&gt;Fly.io&lt;/a&gt;. I even taught myself Electron and built a macOS &lt;a href="https://datasette.io/desktop"&gt;Datasette Desktop&lt;/a&gt; application, so people could install it without having to think about their Python environment.&lt;/p&gt;
&lt;p&gt;Datasette Lite is my latest attempt at this. Anyone with a browser that can run WebAssembly can now run Datasette in it - if they can afford the 10MB load (which in many places with metered internet access is way too much).&lt;/p&gt;
&lt;p&gt;I also built this because I'm fascinated by WebAssembly and I've been looking for an opportunity to really try it out.&lt;/p&gt;
&lt;p&gt;And, I find this project deeply amusing. Running a Python server-side web application in a browser still feels like an absurd thing to do. I love that it works.&lt;/p&gt;
&lt;p&gt;I'm deeply inspired by &lt;a href="https://jupyterlite.readthedocs.io/en/latest/"&gt;JupyterLite&lt;/a&gt;. Datasette Lite's name is a tribute to that project.&lt;/p&gt;
&lt;h4&gt;How it works: Python in a Web Worker&lt;/h4&gt;
&lt;p&gt;Datasette Lite does most of its work in a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers"&gt;Web Worker&lt;/a&gt; - a separate process that can run expensive CPU operations (like an entire Python interpreter) without blocking the main browser's UI thread.&lt;/p&gt;
&lt;p&gt;The worker starts running when you load the page. It loads a WebAssembly compiled Python interpreter from a CDN, then installs Datasette and its dependencies into that interpreter using &lt;a href="https://pyodide.org/en/stable/usage/loading-packages.html#micropip"&gt;micropip&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It also downloads the specified SQLite database files using the browser's HTTP fetching mechanism and writes them to a virtual in-memory filesystem managed by Pyodide.&lt;/p&gt;
&lt;p&gt;Once everything is installed, it imports &lt;code&gt;datasette&lt;/code&gt; and creates a &lt;code&gt;Datasette()&lt;/code&gt; object called &lt;code&gt;ds&lt;/code&gt;. This object stays resident in the web worker.&lt;/p&gt;
&lt;p&gt;To render pages, the &lt;code&gt;index.html&lt;/code&gt; page sends a message to the web worker specifying which Datasette path has been requested - &lt;code&gt;/&lt;/code&gt; for the homepage, &lt;code&gt;/fixtures&lt;/code&gt; for the database index page, &lt;code&gt;/fixtures/facetable&lt;/code&gt; for a table page and so on.&lt;/p&gt;
&lt;p&gt;The web worker then simulates an HTTP GET against that path within Datasette using the following code:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;ds&lt;/span&gt;.&lt;span class="pl-s1"&gt;client&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;, &lt;span class="pl-s1"&gt;follow_redirects&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;This takes advantage of a really useful internal Datasette API: &lt;a href="https://docs.datasette.io/en/stable/internals.html#datasette-client"&gt;datasette.client&lt;/a&gt; is an &lt;a href="https://www.python-httpx.org/"&gt;HTTPX&lt;/a&gt; client object that can be used to execute HTTP requests against Datasette internally, without doing a round-trip across the network.&lt;/p&gt;
&lt;p&gt;I initially added &lt;code&gt;datasette.client&lt;/code&gt; with the goal of making any JSON APIs that Datasette provides available for internal calls by plugins as well, and to make it easier to write automated tests. It turns out to have other interesting applications too!&lt;/p&gt;
&lt;p&gt;The web worker sends a message back to &lt;code&gt;index.html&lt;/code&gt; with the status code, content type and content retrieved from Datasette. JavaScript in &lt;code&gt;index.html&lt;/code&gt; then injects that HTML into the page using &lt;code&gt;.innerHTML&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To get internal links working, Datasette Lite uses a trick I originally learned from jQuery: it applies a capturing event listener to the area of the page displaying the content, such that any link clicks or form submissions will be intercepted by a JavaScript function. That JavaScript can then turn them into new messages to the web worker rather than navigating to another page.&lt;/p&gt;
&lt;h4&gt;Some annotated code&lt;/h4&gt;
&lt;p&gt;Here are annotated versions of the most important pieces of code. In &lt;code&gt;index.html&lt;/code&gt; this code manages the worker and updates the page when it recieves messages from it:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-c"&gt;// Load the worker script&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;datasetteWorker&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;Worker&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"webworker.js"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// Extract the ?url= from the current page's URL&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;initialUrl&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;URLSearchParams&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;location&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;search&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'url'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// Message that to the worker: {type: 'startup', initialUrl: url}&lt;/span&gt;
&lt;span class="pl-s1"&gt;datasetteWorker&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;postMessage&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;'startup'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; initialUrl&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// This function does most of the work - it responds to messages sent&lt;/span&gt;
&lt;span class="pl-c"&gt;// back from the worker to the index page:&lt;/span&gt;
&lt;span class="pl-s1"&gt;datasetteWorker&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;onmessage&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// {type: log, line: ...} messages are appended to a log textarea:&lt;/span&gt;
  &lt;span class="pl-k"&gt;var&lt;/span&gt; &lt;span class="pl-s1"&gt;ta&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getElementById&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'loading-logs'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;type&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s"&gt;'log'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-s1"&gt;loadingLogs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;push&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;line&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;ta&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;value&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;loadingLogs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;join&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"\n"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;ta&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;scrollTop&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;ta&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;scrollHeight&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-k"&gt;return&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;html&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;''&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// If it's an {error: ...} message show it in a &amp;lt;pre&amp;gt; in a &amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-s1"&gt;html&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;`&amp;lt;div style="padding: 0.5em"&amp;gt;&amp;lt;h3&amp;gt;Error&amp;lt;/h3&amp;gt;&amp;lt;pre&amp;gt;&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-en"&gt;escapeHtml&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// If contentType is text/html, show it as straight HTML&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;else&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-pds"&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;span class="pl-cce"&gt;^&lt;/span&gt;text&lt;span class="pl-cce"&gt;\/&lt;/span&gt;html&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;exec&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;contentType&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-s1"&gt;html&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// For contentType of application/json parse and pretty-print it&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;else&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-pds"&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;span class="pl-cce"&gt;^&lt;/span&gt;application&lt;span class="pl-cce"&gt;\/&lt;/span&gt;json&lt;span class="pl-c1"&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;exec&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;contentType&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-s1"&gt;html&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;`&amp;lt;pre style="padding: 0.5em"&amp;gt;&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-en"&gt;escapeHtml&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;parse&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;null&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;4&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&amp;lt;/pre&amp;gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Anything else (likely CSV data) escape it and show in a &amp;lt;pre&amp;gt;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;else&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-s1"&gt;html&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;`&amp;lt;pre style="padding: 0.5em"&amp;gt;&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-en"&gt;escapeHtml&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&amp;lt;/pre&amp;gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Add the result to &amp;lt;div id="output"&amp;gt; using innerHTML&lt;/span&gt;
  &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getElementById&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"output"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerHTML&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;html&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Update the document.title if a &amp;lt;title&amp;gt; element is present&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;title&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getElementById&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"output"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;querySelector&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"title"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;title&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;title&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;title&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerText&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Scroll to the top of the page after each new page is loaded&lt;/span&gt;
  &lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;scrollTo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-c1"&gt;top&lt;/span&gt;: &lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;left&lt;/span&gt;: &lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// If we're showing the initial loading indicator, hide it&lt;/span&gt;
  &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getElementById&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'loading-indicator'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;style&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;display&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;'none'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;webworker.js&lt;/code&gt; script is where the real magic happens:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-c"&gt;// Load Pyodide from the CDN&lt;/span&gt;
&lt;span class="pl-en"&gt;importScripts&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"https://cdn.jsdelivr.net/pyodide/dev/full/pyodide.js"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// Deliver log messages back to the index.html page&lt;/span&gt;
&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;line&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-s1"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;postMessage&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;'log'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;line&lt;/span&gt;: &lt;span class="pl-s1"&gt;line&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;

&lt;span class="pl-c"&gt;// This function initializes Pyodide and installs Datasette&lt;/span&gt;
&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;startDatasette&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;initialUrl&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Mechanism for downloading and saving specified DB files&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;toLoad&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;initialUrl&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;name&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;initialUrl&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;split&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'.db'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;split&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'/'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;slice&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;-&lt;/span&gt;&lt;span class="pl-c1"&gt;1&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;toLoad&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;push&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;name&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;initialUrl&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;else&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// If no ?url= provided, loads these two demo databases instead:&lt;/span&gt;
    &lt;span class="pl-s1"&gt;toLoad&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;push&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"fixtures.db"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"https://latest.datasette.io/fixtures.db"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;toLoad&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;push&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"content.db"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"https://datasette.io/content.db"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-c"&gt;// This does a LOT of work - it pulls down the WASM blob and starts it running&lt;/span&gt;
  &lt;span class="pl-s1"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;pyodide&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;loadPyodide&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;indexURL&lt;/span&gt;: &lt;span class="pl-s"&gt;"https://cdn.jsdelivr.net/pyodide/dev/full/"&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// We need these packages for the next bit of code to work&lt;/span&gt;
  &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;pyodide&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;loadPackage&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'micropip'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;pyodide&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;loadPackage&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'ssl'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;pyodide&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;loadPackage&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'setuptools'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt; &lt;span class="pl-c"&gt;// For pkg_resources&lt;/span&gt;
  &lt;span class="pl-k"&gt;try&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Now we switch to Python code&lt;/span&gt;
    &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;pyodide&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;runPythonAsync&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;
&lt;span class="pl-s"&gt;    # Here's where we download and save those .db files - they are saved&lt;/span&gt;
&lt;span class="pl-s"&gt;    # to a virtual in-memory filesystem provided by Pyodide&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;    # pyfetch is a wrapper around the JS fetch() function - calls using&lt;/span&gt;
&lt;span class="pl-s"&gt;    # it are handled by the browser's regular HTTP fetching mechanism&lt;/span&gt;
&lt;span class="pl-s"&gt;    from pyodide.http import pyfetch&lt;/span&gt;
&lt;span class="pl-s"&gt;    names = []&lt;/span&gt;
&lt;span class="pl-s"&gt;    for name, url in &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;toLoad&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;:&lt;/span&gt;
&lt;span class="pl-s"&gt;        response = await pyfetch(url)&lt;/span&gt;
&lt;span class="pl-s"&gt;        with open(name, "wb") as fp:&lt;/span&gt;
&lt;span class="pl-s"&gt;            fp.write(await response.bytes())&lt;/span&gt;
&lt;span class="pl-s"&gt;        names.append(name)&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;    import micropip&lt;/span&gt;
&lt;span class="pl-s"&gt;    # Workaround for Requested 'h11&amp;lt;0.13,&amp;gt;=0.11', but h11==0.13.0 is already installed&lt;/span&gt;
&lt;span class="pl-s"&gt;    await micropip.install("h11==0.12.0")&lt;/span&gt;
&lt;span class="pl-s"&gt;    # Install Datasette itself!&lt;/span&gt;
&lt;span class="pl-s"&gt;    await micropip.install("datasette==0.62a0")&lt;/span&gt;
&lt;span class="pl-s"&gt;    # Now we can create a Datasette() object that can respond to fake requests&lt;/span&gt;
&lt;span class="pl-s"&gt;    from datasette.app import Datasette&lt;/span&gt;
&lt;span class="pl-s"&gt;    ds = Datasette(names, settings={&lt;/span&gt;
&lt;span class="pl-s"&gt;        "num_sql_threads": 0,&lt;/span&gt;
&lt;span class="pl-s"&gt;    }, metadata = {&lt;/span&gt;
&lt;span class="pl-s"&gt;        # This metadata is displayed in Datasette's footer&lt;/span&gt;
&lt;span class="pl-s"&gt;        "about": "Datasette Lite",&lt;/span&gt;
&lt;span class="pl-s"&gt;        "about_url": "https://github.com/simonw/datasette-lite"&lt;/span&gt;
&lt;span class="pl-s"&gt;    })&lt;/span&gt;
&lt;span class="pl-s"&gt;    `&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-en"&gt;datasetteLiteReady&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;catch&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-s1"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;postMessage&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-c1"&gt;error&lt;/span&gt;: &lt;span class="pl-s1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;

&lt;span class="pl-c"&gt;// Outside promise pattern&lt;/span&gt;
&lt;span class="pl-c"&gt;// https://github.com/simonw/datasette-lite/issues/25#issuecomment-1116948381&lt;/span&gt;
&lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;datasetteLiteReady&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;readyPromise&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;Promise&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-k"&gt;function&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;resolve&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-s1"&gt;datasetteLiteReady&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;resolve&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;// This function handles messages sent from index.html to webworker.js&lt;/span&gt;
&lt;span class="pl-s1"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;onmessage&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// The first message should be that startup message, carrying the URL&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;type&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s"&gt;'startup'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;startDatasette&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;initialUrl&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-k"&gt;return&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-c"&gt;// This promise trick ensures that we don't run the next block until we&lt;/span&gt;
  &lt;span class="pl-c"&gt;// are certain that startDatasette() has finished and the ds.client&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Python object is ready to use&lt;/span&gt;
  &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;readyPromise&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Run the reuest in Python to get a status code, content type and text&lt;/span&gt;
  &lt;span class="pl-k"&gt;try&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;status&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;contentType&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;pyodide&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;runPythonAsync&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
      &lt;span class="pl-s"&gt;`&lt;/span&gt;
&lt;span class="pl-s"&gt;      import json&lt;/span&gt;
&lt;span class="pl-s"&gt;      # ds.client.get(path) simulates running a request through Datasette&lt;/span&gt;
&lt;span class="pl-s"&gt;      response = await ds.client.get(&lt;/span&gt;
&lt;span class="pl-s"&gt;          # Using json here is a quick way to generate a quoted string&lt;/span&gt;
&lt;span class="pl-s"&gt;          &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;event&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-s"&gt;          # If Datasette redirects to another page we want to follow that&lt;/span&gt;
&lt;span class="pl-s"&gt;          follow_redirects=True&lt;/span&gt;
&lt;span class="pl-s"&gt;      )&lt;/span&gt;
&lt;span class="pl-s"&gt;      [response.status_code, response.headers.get("content-type"), response.text]&lt;/span&gt;
&lt;span class="pl-s"&gt;      `&lt;/span&gt;
    &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Message the results back to index.html&lt;/span&gt;
    &lt;span class="pl-s1"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;postMessage&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;status&lt;span class="pl-kos"&gt;,&lt;/span&gt; contentType&lt;span class="pl-kos"&gt;,&lt;/span&gt; text&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;catch&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// If an error occurred, send that back as a {error: ...} message&lt;/span&gt;
    &lt;span class="pl-s1"&gt;self&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;postMessage&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-c1"&gt;error&lt;/span&gt;: &lt;span class="pl-s1"&gt;error&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One last bit of code: here's the JavaScript in &lt;code&gt;index.html&lt;/code&gt; which intercepts clicks on links and turns them into messages to the worker:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;output&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getElementById&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'output'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-c"&gt;// This captures any click on any element within &amp;lt;div id="output"&amp;gt;&lt;/span&gt;
&lt;span class="pl-s1"&gt;output&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addEventListener&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'click'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;ev&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// .closest("a") traverses up the DOM to find if this is an a&lt;/span&gt;
  &lt;span class="pl-c"&gt;// or an element nested in an a. We ignore other clicks.&lt;/span&gt;
  &lt;span class="pl-k"&gt;var&lt;/span&gt; &lt;span class="pl-s1"&gt;link&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;ev&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;srcElement&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;closest&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"a"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;link&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="pl-s1"&gt;link&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;href&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// It was a click on a &amp;lt;a href="..."&amp;gt; link! Cancel the event:&lt;/span&gt;
    &lt;span class="pl-s1"&gt;ev&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stopPropagation&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;ev&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;preventDefault&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// I want #fragment links to still work, using scrollIntoView()&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-en"&gt;isFragmentLink&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;link&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;href&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Jump them to that element, but don't update the URL bar&lt;/span&gt;
      &lt;span class="pl-c"&gt;// since we use # in the URL to mean something else&lt;/span&gt;
      &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;fragment&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-c1"&gt;URL&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;link&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;href&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;hash&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;replace&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"#"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;""&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;fragment&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;el&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getElementById&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;fragment&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
        &lt;span class="pl-s1"&gt;el&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;scrollIntoView&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;
      &lt;span class="pl-k"&gt;return&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;href&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;link&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getAttribute&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"href"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Links to external sites should open in a new window&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-en"&gt;isExternal&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;href&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;open&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;href&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-k"&gt;return&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-c"&gt;// It's an internal link navigation - send it to the worker&lt;/span&gt;
    &lt;span class="pl-en"&gt;loadPath&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;href&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;true&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;loadPath&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// We don't want anything after #, and we only want the /path&lt;/span&gt;
  &lt;span class="pl-s1"&gt;path&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;split&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"#"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;replace&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"http://localhost"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;""&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Update the URL with the new # location&lt;/span&gt;
  &lt;span class="pl-s1"&gt;history&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;pushState&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-c1"&gt;path&lt;/span&gt;: &lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"#"&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Plausible analytics, see:&lt;/span&gt;
  &lt;span class="pl-c"&gt;// https://github.com/simonw/datasette-lite/issues/22&lt;/span&gt;
  &lt;span class="pl-s1"&gt;useAnalytics&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="pl-en"&gt;plausible&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'pageview'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-c1"&gt;u&lt;/span&gt;: &lt;span class="pl-s1"&gt;location&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;href&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;replace&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'?url='&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;''&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;replace&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'#'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;'/'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Send a {path: "/path"} message to the worker&lt;/span&gt;
  &lt;span class="pl-s1"&gt;datasetteWorker&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;postMessage&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;path&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;Getting Datasette to work in Pyodide&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://pyodide.org/"&gt;Pyodide&lt;/a&gt; is the secret sauce that makes this all possible. That project provides several key components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A custom WebAssembly build of the core Python interpreter, bundling the standard library (including a compiled WASM version of SQLite)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pyodide.org/en/stable/usage/loading-packages.html#micropip"&gt;micropip&lt;/a&gt; - a package that can install additional Python dependencies by downloading them from &lt;a href="https://pypi.org/"&gt;PyPI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A comprehensive JavaScript to Python bridge, including mechanisms for &lt;a href="https://pyodide.org/en/stable/usage/type-conversions.html"&gt;translating Python objects&lt;/a&gt; to JavaScript and vice-versa&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://pyodide.org/en/stable/usage/api/js-api.html"&gt;JavaScript API&lt;/a&gt; for launching and then managing a Python interpreter process&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I found the documentation on &lt;a href="https://pyodide.org/en/stable/usage/webworker.html"&gt;Using Pyodide in a web worker&lt;/a&gt; particularly helpful.&lt;/p&gt;
&lt;p&gt;I had to make a few changes to Datasette to get it working with Pyodide. My &lt;a href="https://github.com/simonw/datasette/issues/1733"&gt;tracking issue for that&lt;/a&gt; has the full details, but the short version is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ensure each of Datasette's dependencies had a wheel package on PyPI (as opposed to just a &lt;code&gt;.tar.gz&lt;/code&gt;) - &lt;code&gt;micropip&lt;/code&gt; only works with wheels. I ended up removing &lt;code&gt;python-baseconv&lt;/code&gt; as a dependency and replacing &lt;code&gt;click-default-group&lt;/code&gt; with my own &lt;code&gt;click-default-group-wheel&lt;/code&gt; forked package (&lt;a href="https://github.com/simonw/click-default-group-wheel"&gt;repo here&lt;/a&gt;). I got &lt;code&gt;sqlite-utils&lt;/code&gt; working in Pyodide with this change too, see the &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-26-1"&gt;3.26.1 release notes&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Work around an error caused by importing &lt;code&gt;uvicorn&lt;/code&gt;. Since Datasette Lite doesn't actually run its own web server that dependency wasn't necessary, so I changed my code to catch the &lt;code&gt;ImportError&lt;/code&gt; in the right place.&lt;/li&gt;
&lt;li&gt;The biggest change: WebAssembly can't run threads, which means Python can't run threads, which means any attempts to start a thread in Python cause an error. Datasette only uses threads in one place: to execute SQL queries in a thread pool where they won't block the event loop. I added a new &lt;code&gt;--setting num_sql_threads 0&lt;/code&gt; feature for disabling threading entirely, see &lt;a href="https://github.com/simonw/datasette/issues/1735"&gt;issue 1735&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Having made those changes I shipped them in a &lt;a href="https://github.com/simonw/datasette/releases/tag/0.62a0"&gt;Datasette 0.62a0&lt;/a&gt; release. It's this release that Datasette Lite installs from PyPI.&lt;/p&gt;
&lt;h4&gt;Fragment hashes for navigation&lt;/h4&gt;
&lt;p&gt;You may have noticed that as you navigate through Datasette Lite the URL bar updates with URLs that look like the following:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://lite.datasette.io/#/content/pypi_packages?_facet=author"&gt;https://lite.datasette.io/#/content/pypi_packages?_facet=author&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I'm using the &lt;code&gt;#&lt;/code&gt; here to separate out the path within the virtual Datasette instance from the URL to the Datasette Lite application itself.&lt;/p&gt;
&lt;p&gt;Maintaining the state in the URL like this means that the Back and Forward browser buttons work, and also means that users can bookmark pages within the application and share links to them.&lt;/p&gt;
&lt;p&gt;I usually like to avoid &lt;code&gt;#&lt;/code&gt; URLs - the HTML history API makes it possible to use "real" URLs these days, even for JavaScript applications. But in the case of Datasette Lite those URLs wouldn't actually work - if someone attempted to refresh the page or navigate to a link GitHub Pages wouldn't know what file to serve.&lt;/p&gt;
&lt;p&gt;I could run this on my own domain with a catch-all page handler that serves the Datasette Lite HTML and JavaScript no matter what path is requested, but I wanted to keep this as pure and simple as possible.&lt;/p&gt;
&lt;p&gt;This also means I can reserve Datasette Lite's own query string for things like specifying the database to load, and potentially other options in the future.&lt;/p&gt;
&lt;h4&gt;Web Workers or Service Workers?&lt;/h4&gt;
&lt;p&gt;My initial idea for this project was to build it with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers"&gt;Service Workers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Service Workers are some deep, deep browser magic: they let you install a process that can intercept browser traffic to a specific domain (or path within that domain) and run custom code to return a result. Effectively they let you run your own server-side code in the browser itself.&lt;/p&gt;
&lt;p&gt;They're mainly designed for building offline applications, but my hope was that I could use them to offer a full simulation of a server-side application instead.&lt;/p&gt;
&lt;p&gt;Here's my TIL on &lt;a href="https://til.simonwillison.net/service-workers/intercept-fetch"&gt;Intercepting fetch in a service worker&lt;/a&gt; that came out of my initial research.&lt;/p&gt;
&lt;p&gt;I managed to get a server-side JavaScript "hello world" demo working, but when I tried to add Pyodide I ran into some unavoidable road blocks. It turns out Service Workers are very restricted in which APIs they provide - in particular, they don't allow &lt;code&gt;XMLHttpRequest&lt;/code&gt; calls. Pyodide apparently depends on &lt;code&gt;XMLHttpRequest&lt;/code&gt;, so it was unable to run in a Service Worker at all. I &lt;a href="https://github.com/pyodide/pyodide/issues/2432"&gt;filed an issue&lt;/a&gt; about it with the Pyodide project.&lt;/p&gt;
&lt;p&gt;Initially I thought this would block the whole project, but eventually I figured out a way to achieve the same goals using Web Workers instead.&lt;/p&gt;
&lt;h3&gt;Is this an SPA or an MPA?&lt;/h3&gt;
&lt;p&gt;SPAs are Single Page Applications. MPAs are Multi Page Applications. Datasette Lite is a weird hybrid of the two.&lt;/p&gt;
&lt;p&gt;This amuses me greatly.&lt;/p&gt;
&lt;p&gt;Datasette itself is very deliberately architected as a multi page application.&lt;/p&gt;
&lt;p&gt;I think SPAs, as developed over the last decade, have mostly been a mistake. In my experience they take longer to build, have more bugs and provide worse performance than a server-side, multi-page alternative implementation.&lt;/p&gt;
&lt;p&gt;Obviously if you are building Figma or VS Code then SPAs are the right way to go. But most web applications are not Figma, and don't need to be!&lt;/p&gt;
&lt;p&gt;(I used to think Gmail was a shining example of an SPA, but it's so sludgy and slow loading these days that I now see it as more of an argument against the paradigm.)&lt;/p&gt;
&lt;p&gt;Datasette Lite is an SPA wrapper around an MPA. It literally simulates the existing MPA by running it in a web worker.&lt;/p&gt;
&lt;p&gt;It's very heavy - it loads 11MB of assets before it can show you anything. But it also inherits many of the benefits of the underlying MPA: it has obvious distinctions between pages, a deeply interlinked interface, working back and forward buttons, it's bookmarkable and it's easy to maintain and add new features.&lt;/p&gt;
&lt;p&gt;I'm not sure what my conclusion here is. I'm skeptical of SPAs, and now I've built a particularly weird one. Is this even a good idea? I'm looking forward to finding that out for myself.&lt;/p&gt;
&lt;h4&gt;Coming soon: JavaScript!&lt;/h4&gt;
&lt;p&gt;Another amusing detail about Datasette Lite is that the one part of Datasette that doesn't work yet is Datasette's existing JavaScript features!&lt;/p&gt;
&lt;p&gt;Datasette currently makes very sparing use of JavaScript in the UI: it's used to add some drop-down interactive menus (including the handy "cog" menu on column headings) and for a CodeMirror-enhanced SQL editing interface.&lt;/p&gt;
&lt;p&gt;JavaScript is used much more extensively by several popular Datasette plugins, including &lt;a href="https://datasette.io/plugins/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt; and &lt;a href="https://datasette.io/plugins/datasette-vega"&gt;datasette-vega&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Unfortunately none of this works in Datasette Lite at the moment - because I don't yet have a good way to turn &lt;code&gt;&amp;lt;script src="..."&amp;gt;&lt;/code&gt; links into things that can load content from the Web Worker.&lt;/p&gt;
&lt;p&gt;This is one of the reasons I was initially hopeful about Service Workers.&lt;/p&gt;
&lt;p&gt;Thankfully, since Datasette is built on the principles of progressive enhancement this doesn't matter: the application remains usable even if none of the JavaScript enhancements are applied.&lt;/p&gt;
&lt;p&gt;I have an &lt;a href="https://github.com/simonw/datasette-lite/issues/8"&gt;open issue for this&lt;/a&gt;. I welcome suggestions as to how I can get all of Datasette's existing JavaScript working in the new environment with as little effort as possible.&lt;/p&gt;
&lt;h4 id="bonus-shot-scraper"&gt;Bonus: Testing it with shot-scraper&lt;/h4&gt;
&lt;p&gt;In building Datasette Lite, I've committed to making Pyodide a supported runtime environment for Datasette. How can I ensure that future changes I make to Datasette - accidentally introducing a new dependency that doesn't work there for example - don't break in Pyodide without me noticing?&lt;/p&gt;
&lt;p&gt;This felt like a great opportunity to exercise my &lt;a href="https://datasette.io/tools/shot-scraper"&gt;shot-scraper&lt;/a&gt; CLI tool, in particular its ability to run some JavaScript against a page and &lt;a href="https://github.com/simonw/shot-scraper/blob/0.13/README.md#handling-javascript-errors"&gt;pass or fail a CI job&lt;/a&gt; depending on if that JavaScript throws an error.&lt;/p&gt;
&lt;p&gt;Pyodide needs you to run it from a real web server, not just an HTML file saved to disk - so I put together a &lt;a href="https://github.com/simonw/datasette/blob/280ff372ab30df244f6c54f6f3002da57334b3d7/test-in-pyodide-with-shot-scraper.sh"&gt;very scrappy shell script&lt;/a&gt; which builds a Datasette wheel package, starts a localhost file server (using &lt;code&gt;python3 -m http.server&lt;/code&gt;), then uses &lt;code&gt;shot-scraper javascript&lt;/code&gt; to execute a test against it that installs Datasette from the wheel using &lt;code&gt;micropip&lt;/code&gt; and confirms that it can execute a simple SQL query via the JSON API.&lt;/p&gt;
&lt;p&gt;Here's the script in full, with extra comments:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#!&lt;/span&gt;/bin/bash&lt;/span&gt;
&lt;span class="pl-c1"&gt;set&lt;/span&gt; -e
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; I always forget to do this in my bash scripts - without it, any&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; commands that fail in the script won't result in the script itself&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; returning a non-zero exit code. I need it for running tests in CI.&lt;/span&gt;

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Build the wheel - this generates a file with a name similar to&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; dist/datasette-0.62a0-py3-none-any.whl&lt;/span&gt;
python3 -m build

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Find the name of that wheel file, strip off the dist/&lt;/span&gt;
wheel=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;$(&lt;/span&gt;basename &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;$(&lt;/span&gt;ls dist/&lt;span class="pl-k"&gt;*&lt;/span&gt;.whl&lt;span class="pl-pds"&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-pds"&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; $wheel is now datasette-0.62a0-py3-none-any.whl&lt;/span&gt;

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Create a blank index page that loads Pyodide&lt;/span&gt;
&lt;span class="pl-c1"&gt;echo&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&amp;lt;script src="https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; dist/index.html

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Run a localhost web server for that dist/ folder, in the background&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; so we can do more stuff in this script&lt;/span&gt;
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; dist
python3 -m http.server 8529 &lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt;
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; ..

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Now we use shot-scraper to run a block of JavaScript against our&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; temporary web server. This will execute in the context of that&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; index.html page we created earlier, which has loaded Pyodide&lt;/span&gt;
shot-scraper javascript http://localhost:8529/ &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;async () =&amp;gt; {&lt;/span&gt;
&lt;span class="pl-s"&gt;  // Load Pyodide and all of its necessary assets&lt;/span&gt;
&lt;span class="pl-s"&gt;  let pyodide = await loadPyodide();&lt;/span&gt;
&lt;span class="pl-s"&gt;  // We also need these packages for Datasette to work&lt;/span&gt;
&lt;span class="pl-s"&gt;  await pyodide.loadPackage(['micropip', 'ssl', 'setuptools']);&lt;/span&gt;
&lt;span class="pl-s"&gt;  // We need to escape the backticks because of Bash escaping rules&lt;/span&gt;
&lt;span class="pl-s"&gt;  let output = await pyodide.runPythonAsync(&lt;span class="pl-cce"&gt;\`&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;    import micropip&lt;/span&gt;
&lt;span class="pl-s"&gt;    // This is needed to avoid a dependency conflict error&lt;/span&gt;
&lt;span class="pl-s"&gt;    await micropip.install('h11==0.12.0')&lt;/span&gt;
&lt;span class="pl-s"&gt;    // Here we install the Datasette wheel package we created earlier&lt;/span&gt;
&lt;span class="pl-s"&gt;    await micropip.install('http://localhost:8529/&lt;span class="pl-smi"&gt;$wheel&lt;/span&gt;')&lt;/span&gt;
&lt;span class="pl-s"&gt;    // These imports avoid Pyodide errors importing datasette itself&lt;/span&gt;
&lt;span class="pl-s"&gt;    import ssl&lt;/span&gt;
&lt;span class="pl-s"&gt;    import setuptools&lt;/span&gt;
&lt;span class="pl-s"&gt;    from datasette.app import Datasette&lt;/span&gt;
&lt;span class="pl-s"&gt;    // num_sql_threads=0 is essential or Datasette will crash, since&lt;/span&gt;
&lt;span class="pl-s"&gt;    // Pyodide and WebAssembly cannot start threads&lt;/span&gt;
&lt;span class="pl-s"&gt;    ds = Datasette(memory=True, settings={'num_sql_threads': 0})&lt;/span&gt;
&lt;span class="pl-s"&gt;    // Simulate a hit to execute 'select 55 as itworks' and return the text&lt;/span&gt;
&lt;span class="pl-s"&gt;    (await ds.client.get(&lt;/span&gt;
&lt;span class="pl-s"&gt;      '/_memory.json?sql=select+55+as+itworks&amp;amp;_shape=array'&lt;/span&gt;
&lt;span class="pl-s"&gt;    )).text&lt;/span&gt;
&lt;span class="pl-s"&gt;  &lt;span class="pl-cce"&gt;\`&lt;/span&gt;);&lt;/span&gt;
&lt;span class="pl-s"&gt;  // The last expression in the runPythonAsync block is returned, here&lt;/span&gt;
&lt;span class="pl-s"&gt;  // that's the text returned by the simulated HTTP response to the JSON API&lt;/span&gt;
&lt;span class="pl-s"&gt;  if (JSON.parse(output)[0].itworks != 55) {&lt;/span&gt;
&lt;span class="pl-s"&gt;    // This throws if the JSON API did not return the expected result&lt;/span&gt;
&lt;span class="pl-s"&gt;    // shot-scraper turns that into a non-zero exit code for the script&lt;/span&gt;
&lt;span class="pl-s"&gt;    // which will cause the CI task to fail&lt;/span&gt;
&lt;span class="pl-s"&gt;    throw 'Got ' + output + ', expected itworks: 55';&lt;/span&gt;
&lt;span class="pl-s"&gt;  }&lt;/span&gt;
&lt;span class="pl-s"&gt;  // This gets displayed on the console, with a 0 exit code for a pass&lt;/span&gt;
&lt;span class="pl-s"&gt;  return 'Test passed!';&lt;/span&gt;
&lt;span class="pl-s"&gt;}&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Shut down the server we started earlier, by searching for and killing&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; a process that's running on the port we selected&lt;/span&gt;
pkill -f &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;http.server 8529&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pyodide"&gt;pyodide&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-lite"&gt;datasette-lite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cors"&gt;cors&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="javascript"/><category term="projects"/><category term="python"/><category term="webworkers"/><category term="datasette"/><category term="webassembly"/><category term="pyodide"/><category term="datasette-lite"/><category term="cors"/></entry><entry><title>Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker</title><link href="https://simonwillison.net/2021/Sep/23/partytown/#atom-tag" rel="alternate"/><published>2021-09-23T18:29:14+00:00</published><updated>2021-09-23T18:29:14+00:00</updated><id>https://simonwillison.net/2021/Sep/23/partytown/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/adamdbradley/introducing-partytown-run-third-party-scripts-from-a-web-worker-2cnp"&gt;Introducing Partytown 🎉: Run Third-Party Scripts From a Web Worker&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is just spectacularly clever. Partytown is a 6KB JavaScript library that helps you move gnarly poorly performing third-party scripts out of your main page and into a web worker, so they won’t destroy your page performance. The really clever bit is in how it provides sandboxed access to the page DOM: it uses a devious trick where a proxy object provides getters and setters which then make blocking API calls to a separate service worker, using the mostly-forgotten xhr.open(..., false) parameter that turns off the async default for an XMLHttpRequest call.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/Rich_Harris/status/1441101927798820869"&gt;@Rich_Harris&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/serviceworkers"&gt;serviceworkers&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="javascript"/><category term="webworkers"/><category term="serviceworkers"/></entry><entry><title>AVIF has landed</title><link href="https://simonwillison.net/2020/Sep/9/avif-has-landed/#atom-tag" rel="alternate"/><published>2020-09-09T16:49:24+00:00</published><updated>2020-09-09T16:49:24+00:00</updated><id>https://simonwillison.net/2020/Sep/9/avif-has-landed/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://jakearchibald.com/2020/avif-has-landed/"&gt;AVIF has landed&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
AVIF support landed in Chrome 85 a few weeks ago. It’s a new lossy royalty-free image format derived from AV1 video and it’s really impressive—it can achieve similar results to JPEG using a quarter of the file size! Jake digs into AVIF in detail, providing lots of illustrative examples created using the Squoosh online compressor, which now supports AVIF encoding. Jake used the same WebAssembly encoder from Squoosh to decode AVIF images in a web worker so that the demos in his article would work even for browsers that don’t yet support AVIF natively.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/chrome"&gt;chrome&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/images"&gt;images&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jake-archibald"&gt;jake-archibald&lt;/a&gt;&lt;/p&gt;



</summary><category term="chrome"/><category term="images"/><category term="webworkers"/><category term="webassembly"/><category term="jake-archibald"/></entry><entry><title>When should you be using Web Workers?</title><link href="https://simonwillison.net/2019/Jun/15/when-should-you-be-using-web-workers/#atom-tag" rel="alternate"/><published>2019-06-15T04:31:34+00:00</published><updated>2019-06-15T04:31:34+00:00</updated><id>https://simonwillison.net/2019/Jun/15/when-should-you-be-using-web-workers/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://dassur.ma/things/when-workers/"&gt;When should you be using Web Workers?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
85% of worldwide mobile devices are massively less performant than high end iPhones. Surma argues that we should be making aggressive use of Web Workers to keep as much of our JavaScript as possible off the main UI thread, to avoid freezing up the entire interface.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mobile"&gt;mobile&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-performance"&gt;web-performance&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="mobile"/><category term="webworkers"/><category term="web-performance"/></entry><entry><title>WebAssembly at eBay: A Real-World Use Case</title><link href="https://simonwillison.net/2019/May/22/webassembly-ebay-real-world-use-case/#atom-tag" rel="alternate"/><published>2019-05-22T20:30:58+00:00</published><updated>2019-05-22T20:30:58+00:00</updated><id>https://simonwillison.net/2019/May/22/webassembly-ebay-real-world-use-case/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://medium.com/ebaytech/webassembly-at-ebay-a-real-world-use-case-ef888f38b537"&gt;WebAssembly at eBay: A Real-World Use Case&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
eBay used WebAssembly to run a C++ barcode reading library inside a web worker, passing images from the camera in order to provide a barcode scanning interface as part of their mobile web “add listing” page (a feature that had already proved successful in their native mobile apps). This is a great write-up, with lots of detail about how they compiled the library. They ended up running three barcode solutions in parallel web workers—two using WebAssembly, one in pure JavaScript—because their testing showed that racing between three implementations greatly increased the chance of a match due to how the different libraries handled poor quality or out-of-focus images.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/senthil_hi/status/1131252395520929792"&gt;@senthil_hi&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;&lt;/p&gt;



</summary><category term="webworkers"/><category term="webassembly"/></entry><entry><title>An implausibly illustrated introduction to HTML5 Web Workers</title><link href="https://simonwillison.net/2010/Aug/15/workers/#atom-tag" rel="alternate"/><published>2010-08-15T23:14:00+00:00</published><updated>2010-08-15T23:14:00+00:00</updated><id>https://simonwillison.net/2010/Aug/15/workers/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://wearehugh.com/public/2010/08/html5-web-workers/"&gt;An implausibly illustrated introduction to HTML5 Web Workers&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
By Mark Pilgrim.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/html5"&gt;html5&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mark-pilgrim"&gt;mark-pilgrim&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;&lt;/p&gt;



</summary><category term="html5"/><category term="mark-pilgrim"/><category term="webworkers"/><category term="recovered"/></entry><entry><title>Firefox 3.5 for developers</title><link href="https://simonwillison.net/2009/Jun/30/firefox/#atom-tag" rel="alternate"/><published>2009-06-30T18:08:34+00:00</published><updated>2009-06-30T18:08:34+00:00</updated><id>https://simonwillison.net/2009/Jun/30/firefox/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.mozilla.org/en/Firefox_3.5_for_developers"&gt;Firefox 3.5 for developers&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
It’s out today, and the feature list is huge. Highlights include HTML 5 drag ’n’ drop, audio and video elements, offline resources, downloadable fonts, text-shadow, CSS transforms with -moz-transform, localStorage, geolocation, web workers, trackpad swipe events, native JSON, cross-site HTTP requests, text API for canvas, defer attribute for the script element and TraceMonkey for better JS performance!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/audio"&gt;audio&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/browsers"&gt;browsers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/canvas"&gt;canvas&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/crossdomain"&gt;crossdomain&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csstransforms"&gt;csstransforms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dragndrop"&gt;dragndrop&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/firefox"&gt;firefox&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/firefox35"&gt;firefox35&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fonts"&gt;fonts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geolocation"&gt;geolocation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html5"&gt;html5&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/localstorage"&gt;localstorage&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mozilla"&gt;mozilla&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/offlineresources"&gt;offlineresources&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/textshadow"&gt;textshadow&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tracemonkey"&gt;tracemonkey&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/video"&gt;video&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webworkers"&gt;webworkers&lt;/a&gt;&lt;/p&gt;



</summary><category term="audio"/><category term="browsers"/><category term="canvas"/><category term="crossdomain"/><category term="csstransforms"/><category term="dragndrop"/><category term="firefox"/><category term="firefox35"/><category term="fonts"/><category term="geolocation"/><category term="html5"/><category term="javascript"/><category term="json"/><category term="localstorage"/><category term="mozilla"/><category term="offlineresources"/><category term="performance"/><category term="textshadow"/><category term="tracemonkey"/><category term="video"/><category term="webworkers"/></entry></feed>