{"id":344653,"date":"2021-07-26T07:36:13","date_gmt":"2021-07-26T14:36:13","guid":{"rendered":"https:\/\/css-tricks.com\/?p=344653"},"modified":"2021-07-26T12:32:42","modified_gmt":"2021-07-26T19:32:42","slug":"how-i-built-a-cross-platform-desktop-application-with-svelte-redis-and-rust","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/how-i-built-a-cross-platform-desktop-application-with-svelte-redis-and-rust\/","title":{"rendered":"How I Built a Cross-Platform Desktop Application with Svelte, Redis, and Rust"},"content":{"rendered":"\n

At Cloudflare<\/a>, we have a great product called Workers KV<\/a> which is a key-value storage layer that replicates globally. It can handle millions of keys, each of which is accessible from within a Worker script at exceptionally low latencies, no matter where in the world a request is received. Workers KV is amazing \u2014 and so is its pricing<\/a>, which includes a generous free tier.<\/p>\n\n\n\n

However, as a long-time user of the Cloudflare lineup, I have found one thing missing: local introspection<\/strong>. With thousands, and sometimes hundreds of thousands of keys in my applications, I’d often wish there was a way to query all my data, sort it, or just take a look to see what’s actually there.<\/p>\n\n\n\n\n\n\n\n

Well, recently, I was lucky enough to join Cloudflare! Even more so, I joined just before the quarter’s “Quick Wins Week” \u2014 aka, their week-long hackathon. And given that I hadn’t been around long enough to accumulate a backlog (yet), you best believe I jumped on the opportunity to fulfill my own wish.<\/p>\n\n\n\n

So, with the intro out of the way, let me tell you how I built Workers KV GUI, a cross-platform desktop application using Svelte<\/a>, Redis<\/a>, and Rust<\/a>.<\/p>\n\n\n\n

The front-end application<\/h3>\n\n\n

As a web developer, this was the familiar<\/em> part. I’m tempted to call this the “easy part” but, given that you can use any and all<\/em> HTML, CSS, and JavaScript frameworks, libraries, or patterns, choice paralysis can easily set in… which might be familiar, too. If you have a favorite front-end stack, great, use that! For this application, I chose to use Svelte<\/a> because, for me, it certainly makes and keeps things easy.<\/p>\n\n\n\n

Also, as web developers, we expect to bring all our tooling with us. You certainly can! Again, this phase of the project is no different<\/em> than your typical web application development cycle. You can expect to run yarn dev<\/code> (or some variant) as your main command and feel at home. Keeping with an “easy” theme, I’ve elected to use SvelteKit<\/a>, which is Svelte’s official framework and toolkit for building applications. It includes an optimized build system, a great developer experience (including HMR!), a filesystem-based router, and all that Svelte itself<\/em> has to offer.<\/p>\n\n\n\n

As a framework, especially one that takes care of its own tooling, SvelteKit allowed me to purely<\/em> think about my application<\/em> and its requirements. In fact, as far as configuration is concerned, the only thing I had to do was tell SvelteKit that I wanted to build a single-page application (SPA) that only<\/em> runs in the client. In other words, I had to explicitly opt out<\/em> of SvelteKit’s assumption that I wanted a server, which is actually a fair assumption to make since most applications can benefit from server-side rendering. This was as easy as attaching the @sveltejs\/adapter-static<\/code><\/a> package, which is a configuration preset made exactly for this purpose. After installing, this was my entire configuration file:<\/p>\n\n\n\n

\/\/ svelte.config.js\nimport preprocess from 'svelte-preprocess';\nimport adapter from '@sveltejs\/adapter-static';\n\n\/** @type {import('@sveltejs\/kit').Config} *\/\nconst config = {\n  preprocess: preprocess(),\n\n  kit: {\n    adapter: adapter({\n      fallback: 'index.html'\n    }),\n    files: {\n      template: 'src\/index.html'\n    }\n  },\n};\n\nexport default config;<\/code><\/pre>\n\n\n\n

The index.html<\/code> changes are a personal preference. SvelteKit uses app.html<\/code> as a default base template, but old habits die hard.<\/p>\n\n\n\n

It’s only been a few minutes, and my toolchain already knows it’s building a SPA, that there’s a router in place, and a development server is at the ready. Plus, TypeScript, PostCSS, and\/or Sass support is there if I want it (and I do), thanks to svelte-preprocess<\/code>. Ready to rumble!<\/p>\n\n\n\n

The application needed two views:<\/p>\n\n\n\n

  1. a screen to enter connection details (the default\/welcome\/home page)<\/li>
  2. a screen to actually view your data<\/li><\/ol>\n\n\n\n

    In the SvelteKit world, this translates to two “routes” and SvelteKit dictates that these should exist as src\/routes\/index.svelte<\/code> for the home page and src\/routes\/viewer.svelte<\/code> for the data viewer page. In a true web application, this second route would map to the \/viewer<\/code> URL. While this is still the case, I know that my desktop application won’t have a navigation bar, which means that the URL won’t be visible… which means that it doesn’t matter what I call this route, as long as it makes sense to me.<\/p>\n\n\n\n

    \"\"<\/figure>\n\n\n\n

    The contents of these files are mostly irrelevant, at least for this article. For those curious, the entire project is open source<\/a> and if you’re looking for a Svelte or SvelteKit example, I welcome you to take a look. At the risk of sounding like a broken record, the point here is that I’m building a regular web app.<\/p>\n\n\n\n

    At this time, I’m just designing my views and throwing around fake, hard-coded data until I have something that seems to work. I hung out here for about two days, until everything looked nice and all interactivity (button clicks, form submissions, etc.) got fleshed out. I’d call this a “working” app, or a mockup.<\/p>\n\n\n

    Desktop application tooling<\/h3>\n\n\n

    At this point, a fully functional SPA exists. It operates \u2014 and was developed \u2014 in a web browser. Perhaps counterintuitively, this makes it the perfect candidate to become a desktop application! But how?<\/p>\n\n\n\n

    You may have heard of Electron<\/a>. It’s the<\/em> most well-known tool for building cross-platform desktop applications with web technologies. There are a number of massively popular and successful applications built with it: Visual Studio Code, WhatsApp, Atom, and Slack, to name a few. It works by bundling your web assets with its own Chromium<\/a> installation and its own Node.js<\/a> runtime. In other words, when you’re installing an Electron-based application, it’s coming with an extra Chrome browser and an entire programming language (Node.js). These are embedded within the application contents and there’s no avoiding them, as these are dependencies for the application, guaranteeing that it runs consistently everywhere. As you might imagine, there’s a bit of a trade-off with this approach \u2014 applications are fairly massive (i.e. more than 100MB) and use lots of system resources to operate. In order to use the application, an entirely new\/separate Chrome is running in the background \u2014 not quite the same as opening a new tab.<\/p>\n\n\n\n

    Luckily, there are a few alternatives \u2014 I evaluated Svelte NodeGui<\/a> and Tauri<\/a>. Both choices offered significant application size and utilization savings by relying on native renderers<\/em> the operating system offers, instead of embedding a copy of Chrome to do the same work. NodeGui does this by relying on Qt<\/a>, which is another Desktop\/GUI application framework that compiles to native views. However, in order to do this, NodeGui requires some adjustments to your application code in order for it to translate your<\/em> components into Qt<\/em> components. While I’m sure this certainly would have worked, I wasn’t interested in this solution because I wanted to use exactly<\/em> what I already knew, without requiring any<\/em> adjustments to my Svelte files. By contrast, Tauri achieves its savings by wrapping the operating system’s native webviewer \u2014 for example, Cocoa\/WebKit on macOS, gtk-webkit2 on Linux, and Webkit via Edge on Windows. Webviewers are effectively browsers, which Tauri uses because they already exist on your system, and this means that our applications can remain pure web development products.<\/p>\n\n\n\n

    With these savings, the bare minimum Tauri application is less than 4MB, with average applications weighing less than 20MB. In my testing, the bare minimum NodeGui application weighed about 16MB. A bare minimum Electron app is easily 120MB.<\/p>\n\n\n\n

    Needless to say, I went with Tauri. By following the Tauri Integration<\/a> guide, I added the @tauri-apps\/cli<\/code> package to my devDependencies<\/code> and initialized the project:<\/p>\n\n\n\n

    yarn add --dev @tauri-apps\/cli\nyarn tauri init<\/code><\/pre>\n\n\n\n

    This creates a src-tauri<\/code> directory alongside the src<\/code> directory (where the Svelte application lives). This is where all Tauri-specific files live, which is nice for organization.<\/p>\n\n\n\n

    I had never built a Tauri application before, but after looking at its configuration documentation<\/a>, I was able to keep most of the defaults \u2014 aside from items like the package.productName<\/code> and windows.title<\/code> values, of course. Really, the only changes I needed<\/em> to make were to the build<\/code> config, which had to align with SvelteKit for development and output information:<\/p>\n\n\n\n

    \/\/ src-tauri\/tauri.conf.json\n{\n  \"package\": {\n    \"version\": \"0.0.0\",\n    \"productName\": \"Workers KV\"\n  },\n  \"build\": {\n    \"distDir\": \"..\/build\",\n    \"devPath\": \"http:\/\/localhost:3000\",\n    \"beforeDevCommand\": \"yarn svelte-kit dev\",\n    \"beforeBuildCommand\": \"yarn svelte-kit build\"\n  },\n  \/\/ ...\n}<\/code><\/pre>\n\n\n\n

    The distDir<\/code> relates to where the built production-ready assets are located. This value is resolved from the tauri.conf.json<\/code> file location, hence the ..\/<\/code> prefix.<\/p>\n\n\n\n

    The devPath<\/code> is the URL to proxy during development. By default, SvelteKit spawns a devserver on port 3000 (configurable, of course). I had been visiting the localhost:3000<\/code> address in my browser during the first phase, so this is no different.<\/p>\n\n\n\n

    Finally, Tauri has its own<\/em> dev<\/code> and build<\/code> commands. In order to avoid the hassle of juggling multiple commands or build scripts, Tauri provides the beforeDevCommand<\/code> and beforeBuildCommand<\/code> hooks which allow you to run any command before<\/em> the tauri<\/code> command runs. This is a subtle but strong convenience!<\/p>\n\n\n\n

    The SvelteKit CLI is accessed through the svelte-kit<\/code> binary name. Writing yarn svelte-kit build<\/code>, for example, tells yarn<\/code> to fetch its local svelte-kit<\/code> binary, which was installed via a devDependency<\/code>, and then tells SvelteKit to run its build<\/code> command.<\/p>\n\n\n\n

    With this in place, my root-level package.json<\/code> contained the following scripts:<\/p>\n\n\n\n

    {\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"tauri dev\",\n    \"build\": \"tauri build\",\n    \"prebuild\": \"premove build\",\n    \"preview\": \"svelte-kit preview\",\n    \"tauri\": \"tauri\"\n  },\n  \/\/ ...\n  \"devDependencies\": {\n    \"@sveltejs\/adapter-static\": \"1.0.0-next.9\",\n    \"@sveltejs\/kit\": \"1.0.0-next.109\",\n    \"@tauri-apps\/api\": \"1.0.0-beta.1\",\n    \"@tauri-apps\/cli\": \"1.0.0-beta.2\",\n    \"premove\": \"3.0.1\",\n    \"svelte\": \"3.38.2\",\n    \"svelte-preprocess\": \"4.7.3\",\n    \"tslib\": \"2.2.0\",\n    \"typescript\": \"4.2.4\"\n  }\n}<\/code><\/pre>\n\n\n\n

    After integration, my production command was still yarn build<\/code>, which invokes tauri build<\/code> to actually bundle the desktop application, but only after yarn svelte-kit build<\/code> has completed successfully (via the beforeBuildCommand<\/code> option). And my development command remained yarn dev<\/code> which spawns the tauri dev<\/code> and yarn svelte-kit dev<\/code> commands to run in parallel. The development workflow is entirely within the Tauri application, which is now proxying localhost:3000<\/code>, allowing me to still reap the benefits of a HMR development server.<\/p>\n\n\n\n

    Important:<\/strong> Tauri is still in beta at the time of this writing. That said, it feels very stable and well-planned. I have no affiliation with the project, but it seems like Tauri 1.0 may enter a stable release sooner rather than later. I found the Tauri Discord<\/a> to be very active and helpful, including replies from the Tauri maintainers! They even entertained some of my noob Rust questions throughout the process. :)<\/p>\n\n\n

    Connecting to Redis<\/h3>\n\n\n

    At this point, it’s Wednesday afternoon of Quick Wins week, and \u2014 to be honest \u2014 I’m starting to get nervous about finishing before the team presentation on Friday. Why? Because I’m already halfway through the week, and even though I have a good-looking SPA inside<\/em> a working desktop application, it still doesn’t do anything<\/em>. I’ve been looking at the same fake data<\/em> all week.<\/p>\n\n\n\n

    You may be thinking that because<\/em> I have access to a webview, I can use fetch()<\/code> to make some authenticated REST API calls for the Workers KV data I want and dump it all into localStorage<\/code> or an IndexedDB<\/a> table… You’re 100% right!<\/strong> However, that’s not exactly what I had in mind for my desktop application’s use case.<\/p>\n\n\n\n

    Saving all the data into some kind of in-browser storage is totally viable, but it saves it locally to your machine<\/em>. This means that if you have team members trying to do the same thing, everyone<\/em> will have to fetch and save all the data on their own machines<\/em>, too. Ideally, this Workers KV application should have the option to connect to and sync with an external database. That way, when working in team settings, everyone can tune into the same data cache to save time \u2014 and a couple bucks. This starts to matter when dealing with millions of keys which, as mentioned, is not uncommon with Workers KV.<\/p>\n\n\n\n

    Having thought about it for a bit, I decided to use Redis as my backing store because it also<\/em> is a key-value store. This was great because Redis already treats keys as a first-class citizen and offers the sorting and filtering behaviors I wanted (aka, I can pass along the work instead of implementing it myself!). And then, of course, Redis is easy to install and run either locally or in a container, and there are many hosted-Redis-as-service providers out there if someone chooses to go that route.<\/p>\n\n\n\n

    But, how do I connect to it? My app is basically a browser tab running Svelte, right? Yes \u2014 but also so much more than that.<\/p>\n\n\n\n

    You see, part of Electron’s success is that, yes, it guarantees a web app is presented well on every operating system, but it also brings along a Node.js runtime. As a web developer, this was a lot like including a back-end API directly inside my client. Basically the “…but it works on my machine” problem went away because all of the users were (unknowingly) running the exact same localhost<\/code> setup. Through the Node.js layer, you could interact with the filesystem, run servers on multiple ports, or include a bunch of node_modules<\/code> to \u2014 and I’m just spit-balling here \u2014 connect to a Redis instance. Powerful stuff.<\/p>\n\n\n\n

    We don’t lose this superpower because we’re using Tauri! It’s the same, but slightly different.<\/p>\n\n\n\n

    Instead of including a Node.js runtime, Tauri applications are built with Rust<\/a>, a low-level systems language. This is how Tauri itself<\/em> interacts with the operating system and “borrows” its native webviewer. All of the Tauri toolkit is compiled (via Rust), which allows the built application to remain small and efficient. However, this also means that we<\/em>, the application developers, can include any additional crates<\/a> \u2014 the “npm module” equivalent \u2014 into the built application. And, of course, there’s an aptly named redis<\/code><\/a> crate that, as a Redis client driver, allows the Workers KV GUI to connect to any Redis instance.<\/p>\n\n\n\n

    In Rust, the Cargo.toml<\/code> file is similar to our package.json<\/code> file. This is where dependencies and metadata are defined. In a Tauri setting, this is located at src-tauri\/Cargo.toml<\/code> because, again, everything<\/em> related to Tauri is found in this directory. Cargo also has a concept of “feature flags” defined at the dependency level. (The closest analogy I can come up with is using npm<\/code> to access a module’s internals or import a named submodule, though it’s not quite the same still since, in Rust, feature flags affect how the package is built.)<\/p>\n\n\n\n

    # src-tauri\/Cargo.toml\n[dependencies]\nserde_json = \"1.0\"\nserde = { version = \"1.0\", features = [\"derive\"] }\ntauri = { version = \"1.0.0-beta.1\", features = [\"api-all\", \"menu\"] }\nredis = { version = \"0.20\", features = [\"tokio-native-tls-comp\"] }<\/code><\/pre>\n\n\n\n

    The above defines the redis<\/code> crate as a dependency and opts into the \"tokio-native-tls-comp\"<\/code> feature, which the documentation says is required for TLS support.<\/p>\n\n\n\n

    Okay, so I finally had everything I needed. Before Wednesday ended, I had to get my Svelte to talk to my Redis. After poking around a bit, I noticed that all the important stuff seemed to be happening inside the src-tauri\/main.rs<\/code> file. I took note of the #[command]<\/code> macro, which I knew I had seen before in a Tauri example<\/a> earlier in the day, so I studied<\/s> copied the example file in sections, seeing which errors came and went according to the Rust compiler.<\/p>\n\n\n\n

    Eventually, the Tauri application was able to run again, and I learned that the #[command]<\/code> macro is wrapping the underlying function in a way so that it can receive “context” values, if you choose to use them, and receive pre-parsed argument values. Also, as a language, Rust does a lot<\/em> of type casting. For example:<\/p>\n\n\n\n

    use tauri::{command};\n\n#[command]\nfn greet(name: String, age: u8) {\n  println!(\"Hello {}, {} year-old human!\", name, age);\n}<\/code><\/pre>\n\n\n\n

    This creates a greet<\/code> command which, when run,expects<\/em> two arguments: name<\/code> and age<\/code>. When defined, the name<\/code> value is a string value and age<\/code> is a u8<\/code> data type \u2014 aka, an integer. However, if either are missing, Tauri throws an error because the command definition d<\/em>oes<\/em> not<\/em> say anything is allowed to be optional.<\/p>\n\n\n\n

    To actually connect a Tauri command to the application, it has to be defined as part of the tauri::Builder<\/code> composition, found within the main<\/code> function.<\/p>\n\n\n\n

    use tauri::{command};\n\n#[command]\nfn greet(name: String, age: u8) {\n  println!(\"Hello {}, {} year-old human!\", name, age);\n}\n\nfn main() {\n  \/\/ start composing a new Builder chain\n  tauri::Builder::default()\n    \/\/ assign our generated \"handler\" to the chain\n    .invoke_handler(\n      \/\/ piece together application logic\n      tauri::generate_handler![\n        greet, \/\/ attach the command\n      ]\n    )\n    \/\/ start\/initialize the application\n    .run(\n      \/\/ put it all together\n      tauri::generate_context!()\n    )\n    \/\/ print <message> if error while running\n    .expect(\"error while running tauri application\");\n}<\/code><\/pre>\n\n\n\n

    The Tauri application compiles and is aware<\/em> of the fact that it owns a “greet” command. It’s also already controlling a webview (which we’ve discussed) but in doing so<\/em>, it acts as a bridge between the front end (the webview contents) and the back end, which consists of the Tauri APIs and any additional code we’ve written, like the greet<\/code> command. Tauri allows us to send messages across this bridge so that the two worlds can communicate with one another.<\/p>\n\n\n\n

    \"A
    The developer is responsible for webview contents and may optionally include custom Rust modules and\/or define custom commands. Tauri controls the webviewer and the event bridge, including all message serialization and deserialization.<\/figcaption><\/figure>\n\n\n\n

    This “bridge” can be accessed by the front end by importing functionality from any of the (already included) @tauri-apps<\/code> packages, or by relying on the window.__TAURI__<\/code> global, which is available to the entire client-side application. Specifically, we’re interested in the invoke<\/code><\/a> command, which takes a command name and a set of arguments. If there are any arguments, they must be defined as an object where the keys match the parameter names our Rust function expects.<\/p>\n\n\n\n

    In the Svelte layer, this means that we can do something like this in order to call the greet<\/code> command, defined in the Rust layer:<\/p>\n\n\n\n

    <!-- Greeter.svelte -->\n<script>\n  function onclick() {\n    __TAURI__.invoke('greet', {\n      name: 'Alice',\n      age: 32\n    });\n  }\n<\/script>\n\n<button on:click={onclick}>Click Me<\/button><\/code><\/pre>\n\n\n\n

    When this button is clicked, our terminal window (wherever the tauri dev<\/code> command is running) prints:<\/p>\n\n\n\n

    Hello Alice, 32 year-old human!<\/code><\/pre>\n\n\n\n

    Again, this happens because of the println!<\/code> function, which is effectively console.log<\/code> for Rust, that the greet<\/code> command used. It appears in the terminal’s console window \u2014 not the browser console \u2014 because this code still runs on the Rust\/system side of things.<\/p>\n\n\n\n

    It’s also possible to send something back<\/em> to the client from a Tauri command, so let’s change greet<\/code> quickly:<\/p>\n\n\n\n

    use tauri::{command};\n\n#[command]\nfn greet(name: String, age: u8) {\n  \/\/ implicit return, because no semicolon!\n  format!(\"Hello {}, {} year-old human!\", name, age)\n}\n\n\/\/ OR\n\n#[command]\nfn greet(name: String, age: u8) {\n  \/\/ explicit `return` statement, must have semicolon\n  return format!(\"Hello {}, {} year-old human!\", name, age);\n}<\/code><\/pre>\n\n\n\n

    Realizing that I’d be calling invoke<\/code> many times, and being a bit lazy, I extracted a light client-side helper to consolidate things:<\/p>\n\n\n\n

    \/\/ @types\/global.d.ts\n\/\/\/ <reference types=\"@sveltejs\/kit\" \/>\n\ntype Dict<T> = Record<string, T>;\n\ndeclare const __TAURI__: {\n  invoke: typeof import('@tauri-apps\/api\/tauri').invoke;\n}\n\n\/\/ src\/lib\/tauri.ts\nexport function dispatch(command: string, args: Dict<string|number>) {\n  return __TAURI__.invoke(command, args);\n}<\/code><\/pre>\n\n\n\n

    The previous Greeter.svelte<\/code> was then refactored into:<\/p>\n\n\n\n

    <!-- Greeter.svelte -->\n<script lang=\"ts\">\n  import { dispatch } from '$lib\/tauri';\n\n  async function onclick() {\n    let output = await dispatch('greet', {\n      name: 'Alice',\n      age: 32\n    });\n    console.log('~>', output);\n    \/\/=> \"~> Hello Alice, 32 year-old human!\"\n  }\n<\/script>\n\n<button on:click={onclick}>Click Me<\/button><\/code><\/pre>\n\n\n\n

    Great! So now it’s Thursday and I still haven’t written any Redis code, but at least I know how to connect the two halves of my application’s brain together. It was time to comb back through the client-side code and replace all TODO<\/code>s inside event handlers and connect them to the real deal.<\/p>\n\n\n\n

    I will spare you the nitty gritty here, as it’s very application-specific from here on out \u2014 and is mostly<\/em> a story of the Rust compiler giving me a beat down. Plus, spelunking for nitty gritty is exactly why the project is open source<\/a>!<\/p>\n\n\n\n

    At a high-level, once a Redis connection is established using the given details, a SYNC<\/code> button is accessible in the \/viewer<\/code> route. When this button is clicked (and only<\/em> then \u2014 because of costs) a JavaScript function<\/a> is called, which is responsible for connecting to the Cloudflare REST API<\/a> and dispatching a \"redis_set\"<\/code> command for each key. This redis_set<\/code> command<\/a> is defined in the Rust layer \u2014 as are all<\/em> Redis-based commands \u2014 and is responsible for actually writing the key-value pair to Redis.<\/p>\n\n\n\n

    Reading data out of<\/em> Redis is a very similar process, just inverted. For example, when the \/viewer<\/code> started up, all the keys should be listed and ready to go. In Svelte terms, that means I need to dispatch<\/code> a Tauri command when the \/viewer<\/code> component mounts. That happens here<\/a>, almost verbatim. Additionally, clicking on a key name in the sidebar reveals additional “details” about the key, including its expiration (if any), its metadata (if any), and its actual value (if known). Optimizing for cost and network load, we decided that a key’s value should only be fetched on command. This introduces a REFRESH<\/code> button that, when clicked<\/a>, interacts with the REST API once again, then dispatches a command<\/a> so that the Redis client can update that key individually.<\/p>\n\n\n\n

    I don’t mean to bring things to a rushed ending, but once you’ve seen one successful interaction between your JavaScript and Rust code, you’ve seen them all! The rest of my Thursday and Friday morning was just defining new request-reply pairs, which felt a lot like sending PING<\/code> and PONG<\/code> messages to myself.<\/p>\n\n\n

    Conclusion<\/h3>\n\n\n

    For me \u2014 and I imagine many other JavaScript developers \u2014 the challenge this past week was learning Rust. I’m sure you’ve heard this before and you’ll undoubtedly hear it again. Ownership rules, borrow-checking, and the meanings of single-character syntax markers (which are not easy to search for, by the way) are just a few of the roadblocks that I bumped into. Again, a massive thank-you to the Tauri Discord<\/a> for their help and kindness!<\/p>\n\n\n\n

    This is also to say that using Tauri was not a challenge<\/em> \u2014 it was a massive relief. I definitely plan to use Tauri again in the future, especially knowing that I can use just the webviewer<\/strong> if I want to. Digging into and\/or adding Rust parts was “bonus material” and is only required if my app<\/em> requires it.<\/p>\n\n\n\n

    For those wondering, because I couldn’t find another place to mention it: on macOS, the Workers KV GUI application weighs in at less than 13 MB. I am so thrilled<\/strong> with that!<\/p>\n\n\n\n

    And, of course, SvelteKit certainly made this timeline possible. Not only did it save me a half-day-slog configuring my toolbelt, but the instant, HMR development server probably saved me a few hours of manually refreshing the browser \u2014 and then the Tauri viewer.<\/p>\n\n\n\n

    If you’ve made it this far \u2014 that’s impressive! Thank you so much for your time and attention. A reminder that the project is available on GitHub<\/a> and the latest, pre-compiled binaries are always available through its releases page<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"

    At Cloudflare, we have a great product called Workers KV which is a key-value storage layer that replicates globally. It can handle millions of keys, each of which is accessible from within a Worker script at exceptionally low latencies, no matter where in the world a request is received. Workers KV is amazing \u2014 and […]<\/p>\n","protected":false},"author":284529,"featured_media":344661,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_bbp_topic_count":0,"_bbp_reply_count":0,"_bbp_total_topic_count":0,"_bbp_total_reply_count":0,"_bbp_voice_count":0,"_bbp_anonymous_reply_count":0,"_bbp_topic_count_hidden":0,"_bbp_reply_count_hidden":0,"_bbp_forum_subforum_count":0,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"c2c_always_allow_admin_comments":false,"footnotes":"","jetpack_publicize_message":"How I Built a Cross-Platform Desktop Application with Svelte, Redis, and Rust by @lukeed05","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":[]},"categories":[4],"tags":[18956,18955,6782],"jetpack_publicize_connections":[],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2021\/07\/svelte-rust-redis-app.png?fit=1200%2C600&ssl=1","jetpack-related-posts":[{"id":293787,"url":"https:\/\/css-tricks.com\/building-a-full-stack-serverless-application-with-cloudflare-workers\/","url_meta":{"origin":344653,"position":0},"title":"Building a Full-Stack Serverless Application with Cloudflare Workers","date":"August 9, 2019","format":false,"excerpt":"One of my favorite developments in software development has been the advent of serverless. As a developer who has a tendency to get bogged down in the details of deployment and DevOps, it's refreshing to be given a mode of building web applications that simply abstracts scaling and infrastructure away\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2019\/08\/cloudflare-workers-1-1.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":317537,"url":"https:\/\/css-tricks.com\/wordpress-powered-landing-pages-on-a-totally-different-site-via-cloudflare-workers\/","url_meta":{"origin":344653,"position":1},"title":"WordPress-Powered Landing Pages on a Totally Different Site via Cloudflare Workers","date":"July 22, 2020","format":false,"excerpt":"What if you have some content on one site and want to display that content on another site? We can do this in the browser no problem. We can fetch it, and plunk it onto the page. Ajax, right? Ugh. Now we're in client-side rendered site territory, which isn't great\u2026","rel":"","context":"In "Link"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2020\/07\/codepen-blog-editor.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":352487,"url":"https:\/\/css-tricks.com\/learn-how-to-build-true-edge-apps-with-cloudflare-workers-and-fauna\/","url_meta":{"origin":344653,"position":2},"title":"Learn How to Build True Edge Apps With Cloudflare Workers and Fauna","date":"September 23, 2021","format":false,"excerpt":"There is a lot of buzz around apps running on the edge instead of on a centralized server in web development. Running your app on the edge allows your code to be closer to your users, which makes it faster. However, there is a spectrum of edge apps. Many apps\u2026","rel":"","context":"In "Sponsored"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2021\/09\/1_sYt3j9oAKPbaBaNtc0exTQ.png?fit=1024%2C768&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":297067,"url":"https:\/\/css-tricks.com\/what-i-like-about-writing-styles-with-svelte\/","url_meta":{"origin":344653,"position":3},"title":"What I Like About Writing Styles with Svelte","date":"October 23, 2019","format":false,"excerpt":"There\u2019s been a lot of well-deserved hype around Svelte recently, with the project accumulating over 24,000 GitHub stars. Arguably the simplest JavaScript framework out there, Svelte was written by Rich Harris, the developer behind Rollup. There\u2019s a lot to like about Svelte (performance, built-in state management, writing proper markup rather\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2019\/10\/svelte-logo-outline.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":365614,"url":"https:\/\/css-tricks.com\/how-to-serve-a-subdomain-as-a-subdirectory\/","url_meta":{"origin":344653,"position":4},"title":"How to Serve a Subdomain as a Subdirectory","date":"May 6, 2022","format":false,"excerpt":"Let's say you have a website built on a platform that excels at design and it's available at example.com. But that platform falls short at blogging. So you think to yourself, \"What if I could use a different blogging platform and make it available at example.com\/blog?\" Most people would tell\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2022\/05\/cloudflare-proxy.png?fit=1200%2C600&ssl=1&resize=350%2C200","width":350,"height":200},"classes":[]},{"id":238789,"url":"https:\/\/css-tricks.com\/moving-to-a-cdn\/","url_meta":{"origin":344653,"position":5},"title":"We Put Hundreds of Our Client Sites Behind a CDN, and It Worked Out Really Well","date":"March 16, 2016","format":false,"excerpt":"At the agency I work for, we recently put all ~1,100 of our sites behind a CDN. It seems to be working. Unfortunately, I have no idea why or how we did this. Therefore, in the first part of this article, I force our CTO, Joshua Lynch, to explain the\u2026","rel":"","context":"In "Article"","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"featured_media_src_url":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2021\/07\/svelte-rust-redis-app.png?fit=1024%2C512&ssl=1","_links":{"self":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/344653"}],"collection":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/users\/284529"}],"replies":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/comments?post=344653"}],"version-history":[{"count":10,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/344653\/revisions"}],"predecessor-version":[{"id":345420,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/344653\/revisions\/345420"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/media\/344661"}],"wp:attachment":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/media?parent=344653"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/categories?post=344653"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/tags?post=344653"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}