From 23eb6ca71c66b0bfec0fb100d041b2db9e92ea87 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 12 Jun 2023 12:35:59 +0800 Subject: [PATCH] add sw --- deno.json | 4 +-- deno.lock | 12 ++++++++ fresh.gen.ts | 24 +++++++++------ import_map.json | 4 ++- routes/_middleware.ts | 53 ++++++++++++++++++++++++++++++++ routes/api/deploy_id.ts | 10 ++++++ static/.gitignore | 2 ++ static/sw.ts | 57 +++++++++++++++++++++++++++++++++++ utils.ts | 67 +++++++++++++++++++++++++++++++++++++++++ utils_test.ts | 49 ++++++++++++++++++++++++++++++ 10 files changed, 269 insertions(+), 13 deletions(-) create mode 100644 routes/_middleware.ts create mode 100644 routes/api/deploy_id.ts create mode 100644 static/sw.ts diff --git a/deno.json b/deno.json index 32b5786..22e9bdc 100644 --- a/deno.json +++ b/deno.json @@ -2,7 +2,7 @@ "importMap": "./import_map.json", "tasks": { "cache": "deno cache main.ts server-dev.ts", - "server-dev": "deno run -A --unstable --watch=static/,routes/,translation/ server-dev.ts", + "server-dev": "deno run -A --unstable \"--watch=static/*.css,static/*.ts,static/*/,routes/,translation/\" server-dev.ts", "test": "deno test --allow-read=./ --allow-net --allow-write=./ --allow-run=tasklist.exe --unstable", "run": "deno run --allow-read=./ --allow-write=./ --allow-run=tasklist.exe --allow-env=DENO_DEPLOYMENT_ID --allow-net --unstable", "compile": "deno compile --allow-read=./ --allow-write=./ --allow-run=tasklist.exe --allow-env=DENO_DEPLOYMENT_ID --allow-net --unstable", @@ -11,7 +11,7 @@ }, "fmt": { "indentWidth": 4, - "exclude": ["config.json"] + "exclude": ["config.json", "static/sw.js", "static/sw.meta.json"] }, "compilerOptions": { "jsx": "react-jsx", diff --git a/deno.lock b/deno.lock index b48736e..57c15c5 100644 --- a/deno.lock +++ b/deno.lock @@ -121,6 +121,7 @@ "https://deno.land/x/esbuild@v0.17.11/mod.js": "4f4e61964a551d9c0baf5bb19e973cf631cf8c66ddaf01e70070f8a100fc938c", "https://deno.land/x/esbuild@v0.17.11/wasm.d.ts": "dc279a3a46f084484453e617c0cabcd5b8bd1920c0e562e4ea02dfc828c8f968", "https://deno.land/x/esbuild@v0.17.11/wasm.js": "4030e7b50941ec6e06704c6b5f1f6416cc0f7f35f63daf63f184b728bea79a30", + "https://deno.land/x/esbuild@v0.18.0/mod.js": "ef6f332d1d96e5aee3eb12e9e9eccc64d9df4b500fbb1346bac6b8263fafb6c7", "https://deno.land/x/fresh@1.1.6/dev.ts": "a66c7d64be35bcd6a8e12eec9c27ae335044c70363a241f2e36ee776db468622", "https://deno.land/x/fresh@1.1.6/plugins/twind.ts": "c0570d6010e29ba24ee5f43b9d3f1fe735f7fac76d9a3e680c9896373d669876", "https://deno.land/x/fresh@1.1.6/plugins/twind/shared.ts": "023e0ffcd66668753b5049edab0de46e6d66194fb6026c679034b9bbf04ad6f3", @@ -198,6 +199,7 @@ "https://esm.sh/*preact-render-to-string@5.2.6": "88ec8d8706b6a3f1e0fdad3862a2690dcd9b350d87bdc8e7bd0e27fbc0f7d29e", "https://esm.sh/accept-language-parser@1.5.0/": "16d82ee0a75451f75b42d9a20db4da0ccae7ccc8cc09a41c73b4488aba010b94", "https://esm.sh/gh/SortableJS/Sortable@1.15.0/Sortable.min.js": "0a3e3cf471bf4566d7a3f9823b1d0d3b2bd075404e93419cd4074bd23310a9b0", + "https://esm.sh/lifegpc-md5@1.0.3": "07fb48e0e08189de5f370ab8a1fa9d4dfd1cdb31a1843fba7e6f30d1c28575cd", "https://esm.sh/preact-material-components@1.6.1/Button": "bc60923d511c6e2e33a7064339b3e643a9c15e3ef232ab063ef570af2ef83dc8", "https://esm.sh/preact-material-components@1.6.1/Checkbox": "bf34f5cd8c6d015916d854d91aab2caf115463e97be9a461f8dd3370ea11a49c", "https://esm.sh/preact-material-components@1.6.1/Dialog": "b0ff8da9c770456748f7e065fecda2fc90f5364ea66cae75ff5f51d57f6a87eb", @@ -320,7 +322,17 @@ "https://esm.sh/v124/twind@0.16.19/deno/twind.mjs": "ac4bf729653ee66349a518ee4949f22e84b7f626e8f74de1066e798a7c1ca12a", "https://esm.sh/v124/twind@0.16.19/sheets/sheets.d.ts": "9cd4663d180023e49d9379777c8926347f3f79bf3bee546e7e5be1524fe55e70", "https://esm.sh/v124/twind@0.16.19/twind.d.ts": "48c49da7d770f1236ec8a9397af053e6fb5a2bedacf431f2189eeecc1468c01b", + "https://esm.sh/v125/@stablelib/binary@1.0.1/denonext/binary.mjs": "3dba03d945d82efa1771daa39193e920171562b0c64868c505bd3e68c14f79f8", + "https://esm.sh/v125/@stablelib/constant-time@1.0.1/denonext/constant-time.mjs": "5a3711ba33bc29b816dd413aad3378f765b656196252f34622277eb955e7c223", + "https://esm.sh/v125/@stablelib/hash@1.0.1/denonext/hash.mjs": "887c7f363d7d9bb5978727f12d6344aabd79d5fc7523f1943271e2e5403d944b", + "https://esm.sh/v125/@stablelib/hash@1.0.1/lib/hash.d.ts": "6562fc4b9b470ee474add192278b9ad7d863b1096f57c7aeb26e6b15a86e51a7", + "https://esm.sh/v125/@stablelib/hmac@1.0.1/denonext/hmac.mjs": "9901cc0f5e96d86a039f964e122d6f130c0a722a86bf6287a1ebbb420f81e964", + "https://esm.sh/v125/@stablelib/int@1.0.1/denonext/int.mjs": "179398595f1fcf9e85c3251583cf23031bf1b5b2040d7ad731de19cdb1e61adc", + "https://esm.sh/v125/@stablelib/wipe@1.0.1/denonext/wipe.mjs": "116b9cbca9c97e0a997d7cc3ebd9e60883d99eaa2345d48cafeb783b44808123", + "https://esm.sh/v125/array-buffer-to-hex@1.0.0/denonext/array-buffer-to-hex.mjs": "e9c4132a6dc310f33ffc362e0becd7ab12d209962f7028ad1b9b58439cd9b35e", "https://esm.sh/v125/gh/SortableJS/Sortable@1.15.0/denonext/Sortable.min.js": "aeba191bb6622c4ad41ae5d8f5d4dd6bf15074f264cb3640ed223fbaf052263b", + "https://esm.sh/v125/lifegpc-md5@1.0.3/denonext/lifegpc-md5.mjs": "332f72d899b7fa2a74c4a1a4f00616fa5543bcfd9a182314b9408501c3b0bb10", + "https://esm.sh/v125/lifegpc-md5@1.0.3/lib/md5.d.ts": "82fd2b0c148e013ade0b46149953270220d0fd61835f2d12a95832fa2e0dcfc0", "https://raw.githubusercontent.com/lucacasonato/esbuild_deno_loader/8031f71afa1bbcd3237a94b11f53a2e5c5c0e7bf/deps.ts": "b7248e5b750be62613a9417f407e65ed43726d83b11f9631d6dbb58634bbd7d1", "https://raw.githubusercontent.com/lucacasonato/esbuild_deno_loader/8031f71afa1bbcd3237a94b11f53a2e5c5c0e7bf/mod.ts": "3e507379372361162f93325a216b86f6098defb5bb60144555b507bca26d061f", "https://raw.githubusercontent.com/lucacasonato/esbuild_deno_loader/8031f71afa1bbcd3237a94b11f53a2e5c5c0e7bf/src/deno.ts": "71bee6b14e72ca193c0686d8b4f1f47d639a64745b6f5c7576f7a3616f436f57", diff --git a/fresh.gen.ts b/fresh.gen.ts index ea28c4a..3e8b450 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -3,22 +3,26 @@ // This file is automatically updated during development when running `dev.ts`. import config from "./deno.json" assert { type: "json" }; -import * as $0 from "./routes/api/config.ts"; -import * as $1 from "./routes/api/exit.ts"; -import * as $2 from "./routes/api/export/gallery/zip/[gid].ts"; -import * as $3 from "./routes/api/task.ts"; -import * as $4 from "./routes/index.tsx"; +import * as $0 from "./routes/_middleware.ts"; +import * as $1 from "./routes/api/config.ts"; +import * as $2 from "./routes/api/deploy_id.ts"; +import * as $3 from "./routes/api/exit.ts"; +import * as $4 from "./routes/api/export/gallery/zip/[gid].ts"; +import * as $5 from "./routes/api/task.ts"; +import * as $6 from "./routes/index.tsx"; import * as $$0 from "./islands/Container.tsx"; import * as $$1 from "./islands/Settings.tsx"; import * as $$2 from "./islands/TaskManager.tsx"; const manifest = { routes: { - "./routes/api/config.ts": $0, - "./routes/api/exit.ts": $1, - "./routes/api/export/gallery/zip/[gid].ts": $2, - "./routes/api/task.ts": $3, - "./routes/index.tsx": $4, + "./routes/_middleware.ts": $0, + "./routes/api/config.ts": $1, + "./routes/api/deploy_id.ts": $2, + "./routes/api/exit.ts": $3, + "./routes/api/export/gallery/zip/[gid].ts": $4, + "./routes/api/task.ts": $5, + "./routes/index.tsx": $6, }, islands: { "./islands/Container.tsx": $$0, diff --git a/import_map.json b/import_map.json index f0ba8d1..ce01a13 100644 --- a/import_map.json +++ b/import_map.json @@ -15,6 +15,8 @@ "$std/": "https://deno.land/std@0.187.0/", "preact-material-components/": "https://esm.sh/preact-material-components@1.6.1/", "accept-language-parser/": "https://esm.sh/accept-language-parser@1.5.0/", - "sortable": "https://esm.sh/gh/SortableJS/Sortable@1.15.0/Sortable.min.js" + "sortable": "https://esm.sh/gh/SortableJS/Sortable@1.15.0/Sortable.min.js", + "esbuild/": "https://deno.land/x/esbuild@v0.18.0/", + "lifegpc-md5": "https://esm.sh/lifegpc-md5@1.0.3" } } diff --git a/routes/_middleware.ts b/routes/_middleware.ts new file mode 100644 index 0000000..81aaaba --- /dev/null +++ b/routes/_middleware.ts @@ -0,0 +1,53 @@ +import { MiddlewareHandlerContext } from "$fresh/server.ts"; +import { build } from "esbuild/mod.js"; +import { join, resolve } from "std/path/mod.ts"; +import { asyncForEach, calFileMd5, checkMapFile } from "../utils.ts"; + +export async function handler(req: Request, ctx: MiddlewareHandlerContext) { + const url = new URL(req.url); + if (url.pathname == "/sw.js") { + const base = import.meta.resolve("../static").slice(8); + const map_file = join(base, "sw.meta.json"); + if (!(await checkMapFile(map_file))) { + const data = await build({ + entryPoints: [join(base, "sw.ts")], + outfile: join(base, "sw.js"), + metafile: true, + }); + const map = data.metafile; + await asyncForEach( + Object.getOwnPropertyNames(map.inputs), + async (k) => { + const p = resolve(k); + if (p !== k) { + map.inputs[p] = map.inputs[k]; + delete map.inputs[k]; + k = p; + } + const data = map.inputs[k]; + data.md5 = await calFileMd5(k); + }, + ); + await asyncForEach( + Object.getOwnPropertyNames(map.outputs), + async (k) => { + const p = resolve(k); + if (p !== k) { + map.outputs[p] = map.outputs[k]; + delete map.outputs[k]; + k = p; + } + const data = map.outputs[k]; + data.md5 = await calFileMd5(k); + }, + ); + await Deno.writeTextFile(map_file, JSON.stringify(map)); + console.log("Rebuild."); + } + } + const res = await ctx.next(); + if (url.pathname == "/sw.js") { + res.headers.delete("etag"); + } + return res; +} diff --git a/routes/api/deploy_id.ts b/routes/api/deploy_id.ts new file mode 100644 index 0000000..2a71664 --- /dev/null +++ b/routes/api/deploy_id.ts @@ -0,0 +1,10 @@ +import { Handlers } from "$fresh/server.ts"; + +export const handler: Handlers = { + GET(_req, _ctx) { + const data = { id: Deno.env.get("DENO_DEPLOYMENT_ID") }; + return new Response(JSON.stringify(data), { + headers: { "Content-Type": "application/json" }, + }); + }, +}; diff --git a/static/.gitignore b/static/.gitignore index ccc9229..f9d4f00 100644 --- a/static/.gitignore +++ b/static/.gitignore @@ -1 +1,3 @@ preact-material-components/ +sw.js +sw.meta.json diff --git a/static/sw.ts b/static/sw.ts new file mode 100644 index 0000000..1592bbe --- /dev/null +++ b/static/sw.ts @@ -0,0 +1,57 @@ +import { + ActivateEvent, +} from "https://gist.githubusercontent.com/ithinkihaveacat/227bfe8aa81328c5d64ec48f4e4df8e5/raw/f69f0783e69f5827b20dbe3f3509ddbf73933768/service-worker.d.ts"; + +async function get_deploy_id(): Promise { + const re = await (await fetch("/api/deploy_id")).json(); + return re.id; +} + +let deploy_id: string | undefined = undefined; + +const deleteCache = async (key: string) => { + await caches.delete(key); +}; + +const deleteOldCaches = async () => { + deploy_id = await get_deploy_id(); + const keyList = await caches.keys(); + const cachesToDelete = keyList.filter((key) => key !== deploy_id); + await Promise.all(cachesToDelete.map(deleteCache)); +}; + +/**@ts-ignore */ +self.addEventListener("activate", (event: ActivateEvent) => { + event.waitUntil(deleteOldCaches()); +}); + +const CACHES = ["/common.css", "/preact-material-components/style.css"]; + +function match_url(u: URL) { + const pn = u.pathname; + const ori = u.origin; + if (ori == self.location.origin) { + if (CACHES.includes(pn)) return true; + if (pn.startsWith("/_frsh/")) return true; + } + if (ori === "https://fonts.gstatic.com") return true; + return false; +} + +/**@ts-ignore */ +self.addEventListener("fetch", async (e: FetchEvent) => { + const r = e.request; + const responseFromCache = await caches.match(r); + if (responseFromCache) { + return responseFromCache; + } + const res = await fetch(r); + if (res.ok) { + const url = new URL(r.url); + if (deploy_id && match_url(url)) { + const cache = await caches.open(deploy_id); + await cache.put(r, res.clone()); + } + } + return res; +}); diff --git a/utils.ts b/utils.ts index 9719714..a746ff9 100644 --- a/utils.ts +++ b/utils.ts @@ -2,6 +2,7 @@ import { exists, existsSync } from "std/fs/exists.ts"; import { extname } from "std/path/mod.ts"; import { initParser } from "deno_dom/deno-dom-wasm-noinit.ts"; import { configure } from "zipjs/index.js"; +import { MD5 } from "lifegpc-md5"; export function sleep(time: number): Promise { return new Promise((r) => { @@ -164,3 +165,69 @@ export function add_suffix_to_path(path: string, suffix?: string) { return `${path}-${suffix}`; } } + +export async function calFileMd5(p: string | URL) { + const h = new MD5(); + const f = await Deno.open(p); + try { + const buf = new Uint8Array(4096); + let readed: number | null = null; + do { + readed = await f.read(buf); + if (readed) { + h.update(buf.slice(0, readed)); + } + } while (readed !== null); + return h.digest_hex(); + } finally { + f.close(); + } +} + +export async function checkMapFile(p: string | URL, signal?: AbortSignal) { + if (!(await exists(p))) return false; + const map = JSON.parse(await Deno.readTextFile(p, { signal })); + if ( + !(await asyncEvery( + Object.getOwnPropertyNames(map.inputs), + async (k) => { + const data = map.inputs[k]; + const md5 = data.md5; + if (!md5) return false; + if (!(await exists(k))) return false; + const m = await calFileMd5(k); + return md5 === m; + }, + )) + ) return false; + if ( + !(await asyncEvery( + Object.getOwnPropertyNames(map.outputs), + async (k) => { + const data = map.outputs[k]; + const md5 = data.md5; + if (!md5) return false; + if (!(await exists(k))) return false; + const m = await calFileMd5(k); + return md5 === m; + }, + )) + ) return false; + return true; +} + +export async function asyncEvery( + arr: ArrayLike, + callback: ( + this: V | undefined, + element: T, + index: number, + array: ArrayLike, + ) => Promise, + thisArg?: V, +) { + for (let i = 0; i < arr.length; i++) { + if (!await callback.apply(thisArg, [arr[i], i, arr])) return false; + } + return true; +} diff --git a/utils_test.ts b/utils_test.ts index 55939a9..16fe149 100644 --- a/utils_test.ts +++ b/utils_test.ts @@ -2,13 +2,17 @@ import { assert, assertEquals } from "std/testing/asserts.ts"; import { check_running } from "./pid_check.ts"; import { add_suffix_to_path, + asyncEvery, asyncFilter, asyncForEach, + calFileMd5, filterFilename, promiseState, PromiseStatus, sleep, + sure_dir, } from "./utils.ts"; +import { md5 } from "lifegpc-md5"; Deno.test("promiseState_test", async () => { const p1 = new Promise((res) => setTimeout(() => res(100), 100)); @@ -79,3 +83,48 @@ Deno.test("add_suffix_to_path_test", () => { assert(t.startsWith("test-")); assert(t.endsWith(".ts")); }); + +Deno.test("calFileMd5_test", async () => { + await sure_dir(); + const text = `Hello World.te${Math.random()}`; + await Deno.writeTextFile("./test/test.txt", text); + assertEquals(await calFileMd5("./test/test.txt"), md5(text)); +}); + +Deno.test("asyncEvery_test", async () => { + const e = [new Promise((res) => setTimeout(() => res(100), 100))]; + const e2 = [ + new Promise((res) => setTimeout(() => res(100), 100)), + new Promise((res) => setTimeout(() => res(200), 100)), + ]; + const e3 = [ + new Promise((res) => setTimeout(() => res(100), 100)), + new Promise((res) => setTimeout(() => res(200), 100)), + new Promise((res) => setTimeout(() => res(150), 100)), + ]; + const t = { test: 2 }; + assertEquals( + await asyncEvery(e, async function (e) { + assertEquals(this, t); + return (await e) === 100; + }, t), + true, + ); + assertEquals( + await asyncEvery(e2, async function (e) { + assertEquals(this, t); + const d = await e; + return d === 100; + }, t), + false, + ); + assertEquals( + await asyncEvery(e3, async function (e) { + assertEquals(this, t); + const d = await e; + if (d === 150) throw Error("Should exited."); + return d === 100; + }, t), + false, + ); +});