diff --git a/app/web/src/base/page/all.tsx b/app/web/src/base/page/all.tsx index 4a19a9ed..428ebc33 100644 --- a/app/web/src/base/page/all.tsx +++ b/app/web/src/base/page/all.tsx @@ -6,8 +6,9 @@ export default page({ url: "**", component: ({}) => { useEffect(() => { + if (localStorage.getItem("prasi-session")) { - navigate("/editor"); + navigate("/editor/"); } else { navigate("/login"); } diff --git a/app/web/src/base/page/editor.tsx b/app/web/src/base/page/editor.tsx index 0d04e743..bd03f58c 100644 --- a/app/web/src/base/page/editor.tsx +++ b/app/web/src/base/page/editor.tsx @@ -135,10 +135,13 @@ export default page({ const Editor = local.Editor; if (local.loading || !Editor) return ; - navigator.serviceWorker.controller?.postMessage({ - type: "add-cache", - url: location.href, - }); + const sw = navigator.serviceWorker.controller; + if (sw) { + sw.postMessage({ + type: "add-cache", + url: location.href, + }); + } return ( diff --git a/app/web/src/index.tsx b/app/web/src/index.tsx index eb425061..e069c362 100644 --- a/app/web/src/index.tsx +++ b/app/web/src/index.tsx @@ -6,7 +6,8 @@ import { createAPI, createDB, reloadDBAPI } from "./utils/script/init-api"; import { w } from "./utils/types/general"; const start = async () => { - registerServiceWorker(); + const sw = await registerServiceWorker(); + defineReact(); await defineWindow(false); const base = `${location.protocol}//${location.host}`; @@ -15,6 +16,37 @@ const start = async () => { w.api = createAPI(base); w.db = createDB(base); + navigator.serviceWorker.addEventListener("message", (e) => { + if (e.data.type === "activated") { + if (e.data.shouldRefresh && sw) { + sw.unregister().then(() => { + window.location.reload(); + }); + } + } + if (e.data.type === "ready") { + const sw = navigator.serviceWorker.controller; + + if (sw) { + const routes = Object.entries(w.prasiApi[base].apiEntry).map( + ([k, v]: any) => ({ + url: v.url, + name: k, + }) + ); + + sw.postMessage({ + type: "add-cache", + url: location.href, + }); + sw.postMessage({ + type: "define-route", + routes, + }); + } + } + }); + const el = document.getElementById("root"); if (el) { createRoot(el).render(); @@ -24,7 +56,7 @@ const start = async () => { const registerServiceWorker = async () => { if ("serviceWorker" in navigator) { try { - await navigator.serviceWorker.register( + return await navigator.serviceWorker.register( new URL("./sworker.ts", import.meta.url), { type: "module", diff --git a/app/web/src/render/editor/panel/toolbar/center/api/Internal.tsx b/app/web/src/render/editor/panel/toolbar/center/api/Internal.tsx index c2437806..2d4bbfc5 100644 --- a/app/web/src/render/editor/panel/toolbar/center/api/Internal.tsx +++ b/app/web/src/render/editor/panel/toolbar/center/api/Internal.tsx @@ -23,7 +23,7 @@ export const InternalAPI: FC<{ const reloadStatus = () => { if (p.site) { const s = api.srvapi_check.bind({ apiUrl: "https://api.prasi.app" }); - s(p.site.id).then((e) => { + s(p.site.id).then((e: any) => { local.status = e; checkApi(e === "started"); local.render(); diff --git a/app/web/src/sworker.ts b/app/web/src/sworker.ts index bcf6165c..f4d4a85f 100644 --- a/app/web/src/sworker.ts +++ b/app/web/src/sworker.ts @@ -1,21 +1,38 @@ import { manifest, version } from "@parcel/service-worker"; - +import { RadixRouter, createRouter } from "radix3"; const g = { - cache: null as null | Cache, - dev: false, - baseUrl: "", + router: null as null | RadixRouter, + broadcast(msg: any) { + // @ts-ignore + const c: Clients = self.clients; + c.matchAll({ includeUncontrolled: true }).then((clients) => { + clients.forEach((client) => { + client.postMessage(msg); + }); + }); + }, }; async function install() { const cache = await caches.open(version); - g.cache = cache; await cache.addAll(manifest); + g.broadcast({ type: "installed" }); } addEventListener("install", (e) => (e as ExtendableEvent).waitUntil(install())); async function activate() { + let shouldRefresh = false; const keys = await caches.keys(); - await Promise.all(keys.map((key) => key !== version && caches.delete(key))); + await Promise.all( + keys.map(async (key) => { + if (key !== version) { + await caches.delete(key); + shouldRefresh = true; + } + }) + ); + + g.broadcast({ type: "activated", shouldRefresh }); } addEventListener("activate", (e) => (e as ExtendableEvent).waitUntil(activate()) @@ -24,44 +41,29 @@ addEventListener("activate", (e) => addEventListener("fetch", async (evt) => { const e = evt as FetchEvent; - if (g.baseUrl) { - const url = e.request.url; - if (url.startsWith(g.baseUrl)) { + const url = new URL(e.request.url); + + if (g.router) { + const found = g.router.lookup(url.pathname); + if (found) { return; } } e.respondWith( (async () => { - if (!g.cache) { - g.cache = await caches.open(version); - } - - if (!g.baseUrl) { - const keys = await g.cache.keys(); - const url = new URL(keys[0].url); - url.pathname = ""; - g.baseUrl = url.toString(); - } - - const cache = g.cache; - - const r = await cache.match(e.request); + const r = await caches.match(e.request); if (r) { - cache.add(e.request); return r; } - return await fetch(e.request.url); + return fetch(e.request); })() ); }); +g.broadcast({ type: "ready" }); addEventListener("message", async (e) => { const type = e.data.type; - - if (!g.cache) { - g.cache = await caches.open(version); - } - const cache = g.cache; + const cache = await caches.open(version); switch (type) { case "add-cache": @@ -69,5 +71,13 @@ addEventListener("message", async (e) => { await cache.add(e.data.url); } break; + case "define-route": + console.log("defining route", e.data.routes); + g.router = createRouter({ strictTrailingSlash: false }); + for (const route of e.data.routes) { + g.router.insert(route.url, route); + } + await activate(); + break; } }); diff --git a/app/web/src/utils/script/init-api.ts b/app/web/src/utils/script/init-api.ts index ce448203..b5de9a05 100644 --- a/app/web/src/utils/script/init-api.ts +++ b/app/web/src/utils/script/init-api.ts @@ -130,16 +130,27 @@ export const reloadDBAPI = async ( await set(url, JSON.stringify(w.prasiApi[url]), cache); }; + const prasiBase = `${location.protocol}//${location.host}`; try { const found = await get(url, cache); if (found) { w.prasiApi[url] = JSON.parse(found); - forceReload(); + forceReload().catch(() => { + if (url === prasiBase) { + console.error("Failed to load prasi. Reloading..."); + setTimeout(() => location.reload(), 3000); + } + }); } else { await forceReload(); } } catch (e) { console.warn("Failed to load API"); + + if (url === prasiBase) { + console.error("Failed to load prasi. Reloading..."); + setTimeout(() => location.reload(), 3000); + } } }; diff --git a/pkgs/core/index.ts b/pkgs/core/index.ts index 9b9abcf9..579f1289 100644 --- a/pkgs/core/index.ts +++ b/pkgs/core/index.ts @@ -13,6 +13,7 @@ import { prepareApiRoutes } from "./server/api-scan"; g.status = "init"; await createLogger(); +g.api = {}; g.datadir = g.mode === "dev" ? ".data" : "../data"; g.port = parseInt(process.env.PORT || "4550"); g.mode = process.argv.includes("dev") ? "dev" : "prod"; @@ -31,7 +32,7 @@ if (g.db) { }); } -await createServer(); +createServer(); await parcelBuild(); await generateAPIFrm(); await prepareApiRoutes(); diff --git a/pkgs/core/server/api-scan.ts b/pkgs/core/server/api-scan.ts index a8b78ade..8eacc046 100644 --- a/pkgs/core/server/api-scan.ts +++ b/pkgs/core/server/api-scan.ts @@ -22,7 +22,6 @@ export const prepareApiRoutes = async () => { path: importPath.substring((root || path).length + 1), }; g.api[filename] = route; - g.router.insert(route.url.replace(/\*/gi, "**"), g.api[filename]); } catch (e) { g.log.warn( `Failed to import app/srv/api${importPath.substring( diff --git a/pkgs/core/server/create.ts b/pkgs/core/server/create.ts index cbe05ea0..c9fea124 100644 --- a/pkgs/core/server/create.ts +++ b/pkgs/core/server/create.ts @@ -4,14 +4,25 @@ import { dir } from "../utils/dir"; import { g } from "../utils/global"; import { serveAPI } from "./serve-api"; import { WebSocketHandler } from "bun"; +import { waitUntil } from "web-utils/src/wait-until"; -const cache = { static: {} as Record }; +const cache = { + static: {} as Record< + string, + { type: string; content: ReadableStream } + >, +}; export type WSData = { url: URL }; export const createServer = async () => { - g.api = {}; + await waitUntil(() => g.status !== "init"); g.router = createRouter({ strictTrailingSlash: false }); + + for (const route of Object.values(g.api)) { + g.router.insert(route.url.replace(/\*/gi, "**"), route); + } + g.server = Bun.serve({ port: g.port, websocket: { @@ -45,7 +56,6 @@ export const createServer = async () => { }, } as WebSocketHandler, async fetch(req, server) { - if (g.status === "init") return new Response("initializing..."); const url = new URL(req.url); if (wsHandler[url.pathname]) { @@ -71,14 +81,20 @@ export const createServer = async () => { } try { - if (cache.static[url.pathname]) { - return new Response(cache.static[url.pathname]); + const found = cache.static[url.pathname]; + if (found || g.mode === "prod") { + const res = new Response(found.content); + res.headers.set("Content-Type", found.type); } const file = Bun.file(dir.path(`app/static${url.pathname}`)); if ((await file.exists()) && file.type !== "application/octet-stream") { - cache.static[url.pathname] = file; - return new Response(file as any); + cache.static[url.pathname] = { + type: file.type, + content: file.stream(), + }; + const found = cache.static[url.pathname]; + return new Response(found.content); } } catch (e) { g.log.error(e);