app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit b51dcbb24190418359faeb32ff285dd9c9f51c15
parent 33a988d1778983f7c6e771427f73de33ce132b3f
Author: triesap <triesap@radroots.dev>
Date:   Sat, 27 Dec 2025 20:49:33 +0000

app: centralize startup flow and cache external assets

- Add stylesheets submodule and bump packages submodule ref
- Introduce SQL/Geocoder env vars and validate at runtime
- Consolidate IDB/DB/Geocoder bootstrap via app_init helper
- Update service worker to cache wasm/db assets and fix scope-aware registration

Diffstat:
M.gitmodules | 4++++
Mapp/.env.example | 5+++--
Mapp/src/app.css | 3---
Mapp/src/app.html | 8++++++--
Mapp/src/lib/_env.ts | 12++++++++++++
Mapp/src/lib/utils/app/index.ts | 48+++++++++++++++++++++++++++++++++++-------------
Mapp/src/lib/utils/backup/export.ts | 4++--
Mapp/src/lib/utils/config.ts | 6++----
Mapp/src/routes/(app)/+layout.svelte | 14+++++++-------
Mapp/src/routes/(app)/farms/+page.svelte | 4++--
Mapp/src/routes/(app)/profile/+page.svelte | 6+++---
Mapp/src/routes/(cfg)/+layout.svelte | 12+++++++++++-
Mapp/src/routes/(cfg)/setup/+page.svelte | 49++++++++++++++++++++++++++++++++++++++++++++++---
Mapp/src/routes/+layout.svelte | 23+++++++++++++++++++----
Mapp/src/service-worker.js | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Aapp/static/stylesheets | 1+
Mapp/svelte.config.js | 3+++
17 files changed, 235 insertions(+), 56 deletions(-)

diff --git a/.gitmodules b/.gitmodules @@ -1,3 +1,7 @@ [submodule "packages"] path = packages url = git@github.com:radrootslabs/packages.git +[submodule "app/static/stylesheets"] + path = app/static/stylesheets + url = git@github.com:radrootslabs/stylesheets.git + branch = prod diff --git a/app/.env.example b/app/.env.example @@ -2,9 +2,11 @@ VITE_PUBLIC_DEFAULT_RELAYS= VITE_PUBLIC_RADROOTS_API= VITE_PUBLIC_RADROOTS_MEDIA= VITE_PUBLIC_KEYVAL_NAME= +VITE_PUBLIC_SQL_WASM_URL= +VITE_PUBLIC_GEOCODER_DB_URL= VITE_PUBLIC_NOSTR_CLIENT= PORT= VITE_PUBLIC_RADROOTS_RELAY= VITE_PLATFORM_NAME= VITE_PLATFORM_ACCENT= -VITE_PLATFORM_DESCRIPTION= -\ No newline at end of file +VITE_PLATFORM_DESCRIPTION= diff --git a/app/src/app.css b/app/src/app.css @@ -8,9 +8,6 @@ @import "../static/stylesheets/apps-base.css"; @import "../static/stylesheets/apps-ui.css"; -@import "../static/webfonts/sf-pro-rounded/styles.css"; -@import "../static/webfonts/sf-pro-display/styles.css"; - @plugin "daisyui" { themes: os_light, os_dark; } diff --git a/app/src/app.html b/app/src/app.html @@ -7,8 +7,12 @@ <link rel="manifest" href="%sveltekit.assets%/manifest.json" /> <link rel="icon" href="%sveltekit.assets%/favicon.ico" /> - <link rel="stylesheet" type="text/css" href="/phosphor-icons/bold.css" /> - <link rel="stylesheet" type="text/css" href="/phosphor-icons/fill.css" /> + <link rel="stylesheet" type="text/css" href="https://static.radroots.io/icons/phosphor/bold.css" /> + <link rel="stylesheet" type="text/css" href="https://static.radroots.io/icons/phosphor/fill.css" /> + + <link rel="stylesheet" type="text/css" href="https://static.radroots.io/webfonts/sf-pro-display/styles.css" /> + <link rel="stylesheet" type="text/css" href="https://static.radroots.io/webfonts/sf-pro-rounded/styles.css" /> + <link rel="stylesheet" type="text/css" href="/stylesheets/styles-maplibre-gl.css" /> <link rel="stylesheet" type="text/css" href="/stylesheets/styles-superellipse.css" /> diff --git a/app/src/lib/_env.ts b/app/src/lib/_env.ts @@ -10,6 +10,15 @@ if (!RADROOTS_MEDIA || typeof RADROOTS_MEDIA !== 'string') throw new Error('Miss const KEYVAL_NAME = import.meta.env.VITE_PUBLIC_KEYVAL_NAME; if (!KEYVAL_NAME || typeof KEYVAL_NAME !== 'string') throw new Error('Missing env var: VITE_PUBLIC_KEYVAL_NAME'); +const SQL_WASM_URL = import.meta.env.VITE_PUBLIC_SQL_WASM_URL; +if (!SQL_WASM_URL || typeof SQL_WASM_URL !== 'string') throw new Error('Missing env var: VITE_PUBLIC_SQL_WASM_URL'); + +const GEOCODER_DB_URL = import.meta.env.VITE_PUBLIC_GEOCODER_DB_URL; +if (!GEOCODER_DB_URL || typeof GEOCODER_DB_URL !== 'string') throw new Error('Missing env var: VITE_PUBLIC_GEOCODER_DB_URL'); + +const NOSTR_CLIENT = import.meta.env.VITE_PUBLIC_NOSTR_CLIENT; +if (!NOSTR_CLIENT || typeof NOSTR_CLIENT !== 'string') throw new Error('Missing env var: VITE_PUBLIC_NOSTR_CLIENT'); + const RADROOTS_RELAY = import.meta.env.VITE_PUBLIC_RADROOTS_RELAY; if (!RADROOTS_RELAY || typeof RADROOTS_RELAY !== 'string') throw new Error('Missing env var: VITE_PUBLIC_RADROOTS_RELAY'); @@ -27,11 +36,14 @@ const PROD = import.meta.env.MODE === 'production'; export const _env = { PROD, DEFAULT_RELAYS, + GEOCODER_DB_URL, KEYVAL_NAME, + NOSTR_CLIENT, PLATFORM_ACCENT, PLATFORM_DESCRIPTION, PLATFORM_NAME, RADROOTS_API, RADROOTS_MEDIA, RADROOTS_RELAY, + SQL_WASM_URL, } as const; diff --git a/app/src/lib/utils/app/index.ts b/app/src/lib/utils/app/index.ts @@ -7,16 +7,19 @@ import { app_notify } from "@radroots/apps-lib-pwa/stores/app"; import { WebDatastore } from "@radroots/client/datastore"; import { WebFs } from "@radroots/client/fs"; import { WebGeolocation } from "@radroots/client/geolocation"; -import { WebHttp } from "@radroots/http"; +import { IDB_CONFIG_DATASTORE, IDB_CONFIG_KEYSTORE_NOSTR, RADROOTS_IDB_DATABASE, idb_store_bootstrap } from "@radroots/client/idb"; import { WebKeystoreNostr } from "@radroots/client/keystore"; import { WebNotifications } from "@radroots/client/notifications"; import { WebClientRadroots } from "@radroots/client/radroots"; import { WebTangleDatabase } from "@radroots/client/tangle"; import { Geocoder } from "@radroots/geocoder"; +import { WebHttp } from "@radroots/http"; import type { CallbackPromise } from "@radroots/utils"; import { reset_sql_cipher } from "./cipher"; import type { NavigationRoute } from "./routes"; +const { GEOCODER_DB_URL, RADROOTS_API, SQL_WASM_URL } = _env; + const ls_val = get_store(ls); declare const __APP_GIT_HASH__: string; @@ -27,10 +30,7 @@ export const datastore = new WebDatastore( cfg_datastore_key_map, cfg_datastore_key_param_map, cfg_datastore_key_obj_map, - { - database: "radroots-pwa-v1", - store: "radroots.app.datastore" - } + IDB_CONFIG_DATASTORE ); export const fs = new WebFs(); export const geol = new WebGeolocation(); @@ -41,23 +41,23 @@ export const http = new WebHttp({ app_hash: __APP_GIT_HASH__ }); export const notif = new WebNotifications(); -export const radroots = new WebClientRadroots(_env.RADROOTS_API); -export const nostr_keys = new WebKeystoreNostr({ - database: "radroots-pwa-v1", - store: "radroots.security.keystore.nostr" -}); +export const radroots = new WebClientRadroots(RADROOTS_API); +export const nostr_keys = new WebKeystoreNostr(IDB_CONFIG_KEYSTORE_NOSTR); export const db = new WebTangleDatabase({ - cipher_config: cfg_data.sql_cipher + cipher_config: cfg_data.sql_cipher, + sql_wasm_path: SQL_WASM_URL, }); let db_i: Promise<WebTangleDatabase> | null = null; let db_init_promise: Promise<void> | null = null; let geoc_init_promise: Promise<void> | null = null; +let app_init_promise: Promise<void> | null = null; export const db_init = async (): Promise<void> => { if (!db_init_promise) { db_init_promise = (async () => { + await idb_store_bootstrap(RADROOTS_IDB_DATABASE); await db.init(); })(); } @@ -72,7 +72,11 @@ export const db_init = async (): Promise<void> => { export const geoc_init = async (): Promise<void> => { if (!geoc_init_promise) { geoc_init_promise = (async () => { - await geoc.connect(); + const geoc_ready = await geoc.connect({ + wasm_path: SQL_WASM_URL, + database_path: GEOCODER_DB_URL || "/assets/geonames.db" + }); + if (geoc_ready !== true) throw new Error(geoc_ready.err); })(); } try { @@ -83,9 +87,27 @@ export const geoc_init = async (): Promise<void> => { } }; +export const app_init = async (): Promise<void> => { + if (!app_init_promise) { + app_init_promise = (async () => { + await idb_store_bootstrap(RADROOTS_IDB_DATABASE); + await db_init(); + await geoc_init(); + })(); + } + try { + await app_init_promise; + } catch (e) { + app_init_promise = null; + throw e; + } +}; + export const create_db = async (): Promise<WebTangleDatabase> => { if (!db_i) { - const db_client = new WebTangleDatabase(); + const db_client = new WebTangleDatabase({ + sql_wasm_path: SQL_WASM_URL + }); db_i = (async () => { await db_client.init(); return db_client; diff --git a/app/src/lib/utils/backup/export.ts b/app/src/lib/utils/backup/export.ts @@ -1,4 +1,4 @@ -import { datastore, db, db_init, nostr_keys, notif } from "$lib/utils/app"; +import { app_init, datastore, db, nostr_keys, notif } from "$lib/utils/app"; import { ls } from "$lib/utils/i18n"; import { download_json, get_store, handle_err } from "@radroots/apps-lib"; import type { ExportedAppState } from "@radroots/apps-lib-pwa/types/app"; @@ -53,7 +53,7 @@ const export_nostr_keystore_state = async (): Promise<ExportedAppState["nostr_ke }; const export_tangle_db_state = async (): Promise<ExportedAppState["database"]> => { - await db_init(); + await app_init(); const store_key = db.get_store_key(); const backup = await db.export_backup(); if ("err" in backup) throw_err(backup); diff --git a/app/src/lib/utils/config.ts b/app/src/lib/utils/config.ts @@ -1,13 +1,11 @@ import { _env } from "$lib/_env"; import type { AppConfigRole } from "@radroots/apps-lib-pwa/types/app"; +import { IDB_CONFIG_CIPHER_SQL } from "@radroots/client/idb"; import { root_symbol } from "@radroots/utils"; import type { NostrEventTagClient } from "@radroots/nostr"; export const cfg_data = { - sql_cipher: { - database: "radroots-pwa-v1", - store: "radroots.security.cipher.sql", - } + sql_cipher: IDB_CONFIG_CIPHER_SQL } as const; export const _cfg = { diff --git a/app/src/routes/(app)/+layout.svelte b/app/src/routes/(app)/+layout.svelte @@ -1,7 +1,11 @@ <script lang="ts"> - import { db_init, nostr_keys } from "$lib/utils/app"; + import { app_init, db, nostr_keys } from "$lib/utils/app"; import { nostr_login_nip01 } from "@radroots/apps-nostr"; - import { nostr_context_default, nostr_relays_clear, nostr_relays_open } from "@radroots/nostr"; + import { + nostr_context_default, + nostr_relays_clear, + nostr_relays_open, + } from "@radroots/nostr"; import { handle_err, throw_err } from "@radroots/utils"; import { onMount } from "svelte"; import type { LayoutProps } from "./$types"; @@ -11,7 +15,7 @@ onMount(async () => { try { - await init(); + await app_init(); await nostr_init(); } catch (e) { handle_err(e, `on_mount`); @@ -20,10 +24,6 @@ } }); - const init = async (): Promise<void> => { - await db_init(); - }; - const nostr_init = async (): Promise<void> => { if (!data.public_key) throw_err(`*-key_nostr`); const nostr_key = await nostr_keys.read(data.public_key); diff --git a/app/src/routes/(app)/farms/+page.svelte b/app/src/routes/(app)/farms/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { db, db_init, route } from "$lib/utils/app"; + import { app_init, db, route } from "$lib/utils/app"; import { handle_err } from "@radroots/apps-lib"; import { Farms } from "@radroots/apps-lib-pwa"; import type { @@ -13,7 +13,7 @@ let data: LoadData = $state(undefined); onMount(async () => { - await db_init(); + await app_init(); data = await load_data(); }); diff --git a/app/src/routes/(app)/profile/+page.svelte b/app/src/routes/(app)/profile/+page.svelte @@ -1,5 +1,5 @@ <script lang="ts"> - import { db, db_init, fs, nostr_keys, notif, radroots, route } from "$lib/utils/app"; + import { app_init, db, fs, nostr_keys, notif, radroots, route } from "$lib/utils/app"; import { ls } from "$lib/utils/i18n"; import { parse_file_path } from "@radroots/apps-lib"; import { nostr_pubkey } from "@radroots/apps-nostr"; @@ -16,7 +16,7 @@ onMount(async () => { try { // await init(); - await db_init(); + await app_init(); data = await load_data(); } catch (e) { handle_err(e, `on_mount`); @@ -25,7 +25,7 @@ }); const init = async (): Promise<void> => { - await db_init(); + await app_init(); }; const load_data = async (): Promise<IViewProfileData | undefined> => { diff --git a/app/src/routes/(cfg)/+layout.svelte b/app/src/routes/(cfg)/+layout.svelte @@ -1,16 +1,26 @@ <script lang="ts"> + import { app_init } from "$lib/utils/app"; import { handle_err } from "@radroots/apps-lib"; import { onMount } from "svelte"; import type { LayoutProps } from "./$types"; let { children }: LayoutProps = $props(); + let app_ready = $state(false); onMount(async () => { try { + await app_init(); + app_ready = true; } catch (e) { handle_err(e, `on_mount`); } }); </script> -{@render children()} +{#if !app_ready} + <div class={`flex min-h-screen w-full items-center justify-center`}> + <p class={`text-sm`}>Loading...</p> + </div> +{:else} + {@render children()} +{/if} diff --git a/app/src/routes/(cfg)/setup/+page.svelte b/app/src/routes/(cfg)/setup/+page.svelte @@ -95,6 +95,7 @@ type CfgKeyStep = "intro" | "choice" | "add_existing"; let cfg_key_step: CfgKeyStep = $state("intro"); + let cfg_key_loading = $state(false); const cfg_key_step_index = (step: CfgKeyStep): number => { switch (step) { @@ -132,6 +133,7 @@ cfg_role = undefined; cgf_key_opt = undefined; cfg_key_step = "intro"; + cfg_key_loading = false; nostr_key_add_val = ``; profile_name_val = ``; profile_name_valid = false; @@ -271,50 +273,77 @@ }; const handle_new_key_or_add = async (): Promise<void> => { + console.log(`RUNNING NOSTR KEY SETUP `, cgf_key_opt); + if (cgf_key_opt === `nostr_key_add`) + return set_cfg_key_step("add_existing"); + if (cfg_key_loading) return; + cfg_key_loading = true; try { - if (cgf_key_opt === `nostr_key_add`) - return set_cfg_key_step("add_existing"); + console.log(`cfg_key_gen start`, { + view, + cfg_key_step, + cgf_key_opt, + }); const key_created = await create_nostr_key(); + console.log(`cfg_key_gen result`, { key_created }); if (!key_created) return; handle_view(`cfg_profile`); + console.log(`cfg_key_gen view`, { view }); } catch (e) { + console.log(`ERR `, e); handle_err(e, `handle_new_key_or_add`); + } finally { + cfg_key_loading = false; } }; const create_nostr_key = async (): Promise<boolean> => { + console.log(`cfg_key_gen keystore generate start`); const keys_nostr_gen = await nostr_keys.generate(); + console.log(`keys_nostr_gen `, keys_nostr_gen); if ("err" in keys_nostr_gen) { + console.log(`cfg_key_gen keystore generate err`, keys_nostr_gen); await handle_config_err(keys_nostr_gen); return false; } + console.log(`cfg_key_gen keystore generate ok`); const cfg_update = await datastore.update_obj<ConfigData>("cfg_data", { nostr_public_key: keys_nostr_gen.public_key, }); + console.log(`cfg_update `, cfg_update); if ("err" in cfg_update) { + console.log(`cfg_key_gen datastore update err`, cfg_update); await handle_config_err(cfg_update); return false; } + console.log(`cfg_key_gen datastore update ok`); return true; }; const add_nostr_key = async (secret_key: string): Promise<boolean> => { + console.log(`cfg_key_add keystore add start`); const keys_nostr_add = await nostr_keys.add(secret_key); if ("err" in keys_nostr_add) { + console.log(`cfg_key_add keystore add err`, keys_nostr_add); await notif.alert(`${$ls(`common.invalid_key`)}`); return false; } + console.log(`cfg_key_add keystore add ok`); const cfg_update = await datastore.update_obj<ConfigData>("cfg_data", { nostr_public_key: keys_nostr_add.public_key, }); if ("err" in cfg_update) { + console.log(`cfg_key_add datastore update err`, cfg_update); await handle_config_err(cfg_update); return false; } + console.log(`cfg_key_add datastore update ok`); return true; }; const handle_key_add_existing = async (): Promise<void> => { + if (cfg_key_loading) return; + let loading_set = false; try { if (!nostr_key_add_val) return void (await notif.alert( @@ -329,10 +358,18 @@ value: `${$ls(`common.nostr_key`)}`.toLowerCase(), })}`, )); + cfg_key_loading = true; + loading_set = true; + console.log(`cfg_key_add start`, { + view, + cfg_key_step, + }); const key_added = await add_nostr_key(secret_key); + console.log(`cfg_key_add result`, { key_added }); if (!key_added) return; nostr_key_add_val = ``; handle_view(`cfg_profile`); + console.log(`cfg_key_add view`, { view }); } catch (e) { handle_err(e, `handle_key_add_existing`); return void (await notif.alert( @@ -340,6 +377,8 @@ value: `${$ls(`common.nostr_key`)}`.toLowerCase(), })}`, )); + } finally { + if (loading_set) cfg_key_loading = false; } }; @@ -450,6 +489,7 @@ }; const handle_back = async (): Promise<void> => { + if (cfg_key_loading) return; switch (view) { case `cfg_key`: switch (cfg_key_step) { @@ -801,12 +841,15 @@ continue: { label: `${$ls(`common.continue`)}`, disabled: - cfg_key_step === "choice" && !cgf_key_opt, + cfg_key_loading || + (cfg_key_step === "choice" && !cgf_key_opt), + loading: cfg_key_loading, callback: async () => handle_continue(), }, back: { label: `${$ls(`common.back`)}`, visible: cfg_key_step !== "intro", + disabled: cfg_key_loading, callback: async () => handle_back(), }, }} diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte @@ -1,7 +1,8 @@ <script lang="ts"> import { dev, version as kit_version } from "$app/environment"; + import { resolve } from "$app/paths"; import { page } from "$app/state"; - import { db_init, geoc_init } from "$lib/utils/app"; + import { app_init } from "$lib/utils/app"; import { app_cfg } from "$lib/utils/app/config"; import { lc_color_mode, @@ -37,6 +38,7 @@ content: string; }; + const HEAD_META_TAGS: MetaTag[] = [ { name: "app_version", @@ -92,17 +94,30 @@ const register_service_worker = async (): Promise<void> => { if (dev) return; if (!("serviceWorker" in navigator)) return; + const service_worker_root = resolve("/"); + const service_worker_path = service_worker_root.endsWith("/") + ? `${service_worker_root}service-worker.js` + : `${service_worker_root}/service-worker.js`; try { - await navigator.serviceWorker.register("/service-worker.js"); + await navigator.serviceWorker.register(service_worker_path); await navigator.serviceWorker.ready; } catch { return; } }; + const unregister_service_workers = async (): Promise<void> => { + if (!("serviceWorker" in navigator)) return; + const registrations = await navigator.serviceWorker.getRegistrations(); + await Promise.all(registrations.map((registration) => registration.unregister())); + if (!("caches" in globalThis)) return; + const cache_names = await caches.keys(); + await Promise.all(cache_names.map((name) => caches.delete(name))); + }; + onMount(async () => { - await db_init(); - await geoc_init(); + if (dev) await unregister_service_workers(); + await app_init(); await register_service_worker(); }); diff --git a/app/src/service-worker.js b/app/src/service-worker.js @@ -1,6 +1,42 @@ import { build, files, prerendered, version } from "$service-worker"; +import { _env } from "$lib/_env"; +import { DEFAULT_SQL_WASM_PATH } from "@radroots/client/sql/constants"; +import { DEFAULT_GEOCODER_DATABASE_PATH } from "@radroots/geocoder/constants"; -const APP_SHELL_URL = "/index.html"; +const APP_SHELL_URL = new URL(self.registration.scope).pathname; +const normalize_env_path = (value) => + typeof value === "string" && value.trim().length ? value.trim() : undefined; +const parse_env_path = (route_path) => { + const path_end = route_path.search(/[?#]/u); + return path_end >= 0 ? route_path.slice(0, path_end) : route_path; +}; +const ensure_env_wasm_path = (value, env_name) => { + const path = parse_env_path(value); + const normalized = path.toLowerCase(); + if (!normalized || normalized.endsWith("/")) + throw new Error(`${env_name} must include a .wasm filename`); + if (!normalized.endsWith(".wasm")) + throw new Error(`${env_name} must end with .wasm`); + return value; +}; +const ensure_env_asset_path = (value, env_name) => { + const path = parse_env_path(value); + if (!path || path.endsWith("/")) + throw new Error(`${env_name} must include a file path`); + return value; +}; +const SQL_WASM_ENV = normalize_env_path(_env.SQL_WASM_URL); +const SQL_WASM_URL = SQL_WASM_ENV + ? ensure_env_wasm_path( + SQL_WASM_ENV, + "VITE_PUBLIC_SQL_WASM_URL", + ) + : DEFAULT_SQL_WASM_PATH; +const GEOCODER_DB_ENV = normalize_env_path(_env.GEOCODER_DB_URL); +const GEOCODER_DB_URL = GEOCODER_DB_ENV + ? ensure_env_asset_path(GEOCODER_DB_ENV, "VITE_PUBLIC_GEOCODER_DB_URL") + : DEFAULT_GEOCODER_DATABASE_PATH; +const ASSET_URLS = [...new Set([SQL_WASM_URL, GEOCODER_DB_URL])]; const PRECACHE_URLS = [...new Set([...build, ...files, ...prerendered, APP_SHELL_URL])].filter( (url) => !url.includes("/.") ); @@ -10,24 +46,53 @@ const PRECACHE_LIST = PRECACHE_URLS.map((url) => ({ })); const APP_CACHE = `cache-app-shell-v${version}`; const APP_CACHE_PREFIX = "cache-app-shell-v"; +const ASSET_CACHE = "cache-app-assets-v1"; +const ASSET_CACHE_PREFIX = "cache-app-assets-v"; + +const normalize_asset_url = (url) => { + const resolved = new URL(url, self.location.origin); + resolved.search = ""; + resolved.hash = ""; + return resolved.href; +}; +const ASSET_URL_KEYS = new Set(ASSET_URLS.map((url) => normalize_asset_url(url))); +const ASSET_URLS_ABS = ASSET_URLS.map((url) => new URL(url, self.location.origin).href); + +const cache_assets = async () => { + const cache = await caches.open(ASSET_CACHE); + await Promise.all( + ASSET_URLS_ABS.map(async (url) => { + const cached = await cache.match(url); + if (cached) return; + try { + const response = await fetch(url); + if (response.ok || response.type === "opaque") await cache.put(url, response.clone()); + } catch { } + }) + ); +}; + +const is_asset_request = (request_url) => ASSET_URL_KEYS.has(normalize_asset_url(request_url)); const precache = async () => { const cache = await caches.open(APP_CACHE); await cache.addAll(PRECACHE_LIST.map((entry) => entry.url)); + await cache_assets(); }; const cleanup_caches = async () => { const keys = await caches.keys(); for (const key of keys) { - if (!key.startsWith(APP_CACHE_PREFIX)) continue; - if (key === APP_CACHE) continue; + const is_app_cache = key.startsWith(APP_CACHE_PREFIX) && key !== APP_CACHE; + const is_asset_cache = key.startsWith(ASSET_CACHE_PREFIX) && key !== ASSET_CACHE; + if (!is_app_cache && !is_asset_cache) continue; await caches.delete(key); } }; const range_response = async (request, response) => { const range = request.headers.get("range"); - if (!range || !response) return response; + if (!range || !response || response.type === "opaque") return response; const bytes = /bytes=(\d+)-(\d+)?/u.exec(range); if (!bytes) return response; const start = Number(bytes[1]); @@ -42,20 +107,20 @@ const range_response = async (request, response) => { return new Response(sliced, { status: 206, statusText: "Partial Content", headers }); }; -const cache_first = async (request) => { - const cache = await caches.open(APP_CACHE); +const cache_first = async (request, cache_name = APP_CACHE) => { + const cache = await caches.open(cache_name); const cached = await cache.match(request); if (cached) return await range_response(request, cached); const response = await fetch(request); - if (response.ok) await cache.put(request, response.clone()); + if (response.ok || response.type === "opaque") await cache.put(request, response.clone()); return response; }; -const network_first = async (request) => { - const cache = await caches.open(APP_CACHE); +const network_first = async (request, cache_name = APP_CACHE) => { + const cache = await caches.open(cache_name); try { const response = await fetch(request); - if (response.ok) await cache.put(request, response.clone()); + if (response.ok || response.type === "opaque") await cache.put(request, response.clone()); return response; } catch { const cached = await cache.match(request); @@ -78,6 +143,10 @@ self.addEventListener("fetch", (event) => { const request = event.request; if (request.method !== "GET") return; const url = new URL(request.url); + if (is_asset_request(request.url)) { + event.respondWith(cache_first(request, ASSET_CACHE)); + return; + } if (request.mode === "navigate") { event.respondWith(network_first(request)); return; diff --git a/app/static/stylesheets b/app/static/stylesheets @@ -0,0 +1 @@ +Subproject commit ab54196a7e5527b95aadddb01f094d96cbe7968e diff --git a/app/svelte.config.js b/app/svelte.config.js @@ -11,6 +11,9 @@ const config = { precompress: false, strict: true }), + paths: { + relative: false + } } };