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:
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
+ }
}
};