Release Healer Man 0.1.14
This commit is contained in:
1 parent
67d7a8ebe7
commit
7b41b102fa
147 files changed
+169892
-1342
No files matched your search
@@ -0,0 +1,24 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { BROWSER_CONTENT_CACHE_NAME } from "./contentManager";
|
||||
|
||||
const workerSource = readFileSync(
|
||||
new URL("../../public/content-cache-sw.js", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
describe("browser content cache worker", () => {
|
||||
it("serves only cached same-origin asset requests from the active content cache", () => {
|
||||
expect(workerSource).toContain(`const CONTENT_CACHE_NAME = "${BROWSER_CONTENT_CACHE_NAME}";`);
|
||||
expect(workerSource).toContain('request.method !== "GET"');
|
||||
expect(workerSource).toContain("url.origin !== self.location.origin");
|
||||
expect(workerSource).toContain("MANAGED_ASSET_PREFIXES.some");
|
||||
expect(workerSource).toContain('"/assets/game/"');
|
||||
expect(workerSource).toContain("if (cached) return cached");
|
||||
expect(workerSource).toContain("BUNDLED_ASSET_PATHS.has(url.pathname)");
|
||||
expect(workerSource).toContain("BUNDLED_ASSET_PREFIXES.some");
|
||||
expect(workerSource).not.toContain('"/assets/characters/wow335a-v1/textures/character/human/",');
|
||||
expect(workerSource).toContain('status: 404');
|
||||
expect(workerSource).not.toContain("cached ?? fetch(request)");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
BROWSER_CONTENT_CACHE_NAME,
|
||||
checkForContentUpdates,
|
||||
contentPackHasUpdate,
|
||||
contentPackIsBundled,
|
||||
contentPackIsInstalled,
|
||||
contentPackIsUsable,
|
||||
getContentManagerSnapshot,
|
||||
installContentPack,
|
||||
removeContentPack,
|
||||
} from "./contentManager";
|
||||
import type { ContentManifest } from "./contentManifest";
|
||||
|
||||
class MemoryCache {
|
||||
readonly responses = new Map<string, Response>();
|
||||
|
||||
async match(request: RequestInfo | URL): Promise<Response | undefined> {
|
||||
return this.responses.get(this.key(request))?.clone();
|
||||
}
|
||||
|
||||
async put(request: RequestInfo | URL, response: Response): Promise<void> {
|
||||
this.responses.set(this.key(request), response.clone());
|
||||
}
|
||||
|
||||
async delete(request: RequestInfo | URL): Promise<boolean> {
|
||||
return this.responses.delete(this.key(request));
|
||||
}
|
||||
|
||||
private key(request: RequestInfo | URL): string {
|
||||
if (request instanceof Request) return request.url;
|
||||
return new URL(String(request), "http://localhost").toString();
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryCacheStorage {
|
||||
readonly stores = new Map<string, MemoryCache>();
|
||||
|
||||
async open(name: string): Promise<Cache> {
|
||||
const cache = this.stores.get(name) ?? new MemoryCache();
|
||||
this.stores.set(name, cache);
|
||||
return cache as unknown as Cache;
|
||||
}
|
||||
|
||||
async delete(name: string): Promise<boolean> {
|
||||
return this.stores.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
function sha256(bytes: Uint8Array): string {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
function contentManifest(version: string, objectName: string, bytes: Uint8Array): ContentManifest {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
version,
|
||||
createdAt: "2026-09-04T00:00:00.000Z",
|
||||
minimumAppVersion: "0.1.0",
|
||||
packs: [{
|
||||
id: "characters:test",
|
||||
label: "Test characters",
|
||||
bytes: bytes.byteLength,
|
||||
fileCount: 1,
|
||||
bundledOnAndroid: false,
|
||||
dependencies: [],
|
||||
}],
|
||||
files: {
|
||||
"/assets/characters/test.glb": {
|
||||
packId: "characters:test",
|
||||
url: `/content/objects/${objectName}`,
|
||||
sha256: sha256(bytes),
|
||||
bytes: bytes.byteLength,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("browser content packs", () => {
|
||||
it("downloads, updates, and removes verified optional content", async () => {
|
||||
const firstBytes = new TextEncoder().encode("first-model");
|
||||
const updatedBytes = new TextEncoder().encode("updated-model");
|
||||
let manifest = contentManifest("content-one", "first.glb", firstBytes);
|
||||
let corruptUpdate = false;
|
||||
const storage = new MemoryCacheStorage();
|
||||
vi.spyOn(Capacitor, "isNativePlatform").mockReturnValue(false);
|
||||
vi.stubGlobal("caches", storage as unknown as CacheStorage);
|
||||
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/content/manifest.json")) return Response.json(manifest);
|
||||
if (url.endsWith("/first.glb")) return new Response(firstBytes);
|
||||
if (url.endsWith("/updated.glb")) {
|
||||
return new Response(corruptUpdate ? new Uint8Array(updatedBytes.byteLength).fill(7) : updatedBytes);
|
||||
}
|
||||
return new Response(null, { status: 404 });
|
||||
}));
|
||||
|
||||
await checkForContentUpdates();
|
||||
expect(contentPackIsInstalled("characters:test")).toBe(false);
|
||||
expect(contentPackIsUsable("characters:test")).toBe(false);
|
||||
expect(contentPackHasUpdate("characters:test")).toBe(false);
|
||||
|
||||
await installContentPack("characters:test");
|
||||
expect(contentPackIsInstalled("characters:test")).toBe(true);
|
||||
expect(contentPackIsUsable("characters:test")).toBe(true);
|
||||
expect(contentPackHasUpdate("characters:test")).toBe(false);
|
||||
expect(getContentManagerSnapshot()).toMatchObject({
|
||||
installingPackId: null,
|
||||
downloadedBytes: firstBytes.byteLength,
|
||||
totalBytes: firstBytes.byteLength,
|
||||
error: "",
|
||||
});
|
||||
const active = storage.stores.get(BROWSER_CONTENT_CACHE_NAME);
|
||||
expect(await (await active?.match("http://localhost/assets/characters/test.glb"))?.text()).toBe("first-model");
|
||||
|
||||
manifest = contentManifest("content-two", "updated.glb", updatedBytes);
|
||||
await checkForContentUpdates();
|
||||
expect(contentPackHasUpdate("characters:test")).toBe(true);
|
||||
|
||||
corruptUpdate = true;
|
||||
await expect(installContentPack("characters:test")).rejects.toThrow(/verification/i);
|
||||
expect(await (await active?.match("http://localhost/assets/characters/test.glb"))?.text()).toBe("first-model");
|
||||
|
||||
corruptUpdate = false;
|
||||
await installContentPack("characters:test");
|
||||
expect(contentPackHasUpdate("characters:test")).toBe(false);
|
||||
expect(await (await active?.match("http://localhost/assets/characters/test.glb"))?.text()).toBe("updated-model");
|
||||
|
||||
await removeContentPack("characters:test");
|
||||
expect(contentPackIsInstalled("characters:test")).toBe(false);
|
||||
expect(contentPackIsUsable("characters:test")).toBe(false);
|
||||
expect(await active?.match("http://localhost/assets/characters/test.glb")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps default browser content installed and non-removable", async () => {
|
||||
vi.spyOn(Capacitor, "isNativePlatform").mockReturnValue(false);
|
||||
|
||||
expect(contentPackIsBundled("core:ui")).toBe(true);
|
||||
expect(contentPackIsUsable("core:ui")).toBe(true);
|
||||
await expect(removeContentPack("core:ui")).resolves.toBeUndefined();
|
||||
expect(contentPackIsUsable("core:ui")).toBe(true);
|
||||
});
|
||||
});
|
||||
+194
-24
@@ -11,7 +11,23 @@ import { NativeContentStorage } from "./nativeContentStorage";
|
||||
|
||||
const CONTENT_STATE_KEY = "healer-man.content-state.v1";
|
||||
const PRODUCTION_ORIGIN = "https://iwanttoheal.phenomrom.com";
|
||||
const STATIC_ANDROID_BUNDLED_PACKS = new Set(["core:ui", "core:equipment", "dungeon:wailing-caverns"]);
|
||||
const STATIC_ANDROID_BUNDLED_PACKS = new Set([
|
||||
"core:ui",
|
||||
"core:equipment",
|
||||
"core:character-basic",
|
||||
"core:runtime",
|
||||
"dungeon:wailing-caverns",
|
||||
]);
|
||||
const STATIC_BROWSER_BUNDLED_PACKS = new Set([
|
||||
"core:ui",
|
||||
"core:equipment",
|
||||
"core:character-basic",
|
||||
"core:runtime",
|
||||
"dungeon:wailing-caverns",
|
||||
]);
|
||||
|
||||
export const BROWSER_CONTENT_CACHE_NAME = "healer-man-content-v1";
|
||||
export const BROWSER_CONTENT_WORKER_URL = "/content-cache-sw.js";
|
||||
|
||||
export interface InstalledContentFile {
|
||||
readonly packId: string;
|
||||
@@ -29,6 +45,7 @@ interface PersistedContentState {
|
||||
|
||||
export interface ContentManagerSnapshot {
|
||||
readonly revision: number;
|
||||
readonly installedRevision: number;
|
||||
readonly checking: boolean;
|
||||
readonly manifest: ContentManifest | null;
|
||||
readonly bundledManifest: ContentManifest | null;
|
||||
@@ -42,6 +59,7 @@ const EMPTY_STATE: PersistedContentState = { version: 1, packVersions: {}, files
|
||||
let installedState = readInstalledState();
|
||||
let snapshot: ContentManagerSnapshot = {
|
||||
revision: 0,
|
||||
installedRevision: 0,
|
||||
checking: false,
|
||||
manifest: null,
|
||||
bundledManifest: null,
|
||||
@@ -87,13 +105,45 @@ function absoluteContentUrl(value: string): string {
|
||||
return new URL(value, `${contentOrigin() || "http://localhost"}/`).toString();
|
||||
}
|
||||
|
||||
function browserCacheRequest(logicalPath: string): Request {
|
||||
const appOrigin = typeof window === "undefined" ? "http://localhost" : window.location.origin;
|
||||
return new Request(new URL(logicalPath, `${appOrigin}/`).toString(), { method: "GET" });
|
||||
}
|
||||
|
||||
function browserCacheStorage(): CacheStorage | null {
|
||||
return typeof caches === "undefined" ? null : caches;
|
||||
}
|
||||
|
||||
async function registerBrowserContentWorker(): Promise<void> {
|
||||
if (Capacitor.isNativePlatform()
|
||||
|| import.meta.env.DEV
|
||||
|| typeof navigator === "undefined"
|
||||
|| !("serviceWorker" in navigator)) return;
|
||||
try {
|
||||
await navigator.serviceWorker.register(BROWSER_CONTENT_WORKER_URL, {
|
||||
scope: "/",
|
||||
updateViaCache: "none",
|
||||
});
|
||||
await navigator.serviceWorker.ready;
|
||||
} catch (error) {
|
||||
console.warn("Browser content cache worker could not be registered.", error);
|
||||
}
|
||||
}
|
||||
|
||||
function extensionForEntry(entry: ContentManifestFile): string {
|
||||
const match = /(?:\.[A-Za-z0-9]+)(?:\?.*)?$/.exec(entry.url);
|
||||
return match?.[0].split("?")[0].toLowerCase() ?? ".bin";
|
||||
}
|
||||
|
||||
function bundledFiles(): Readonly<Record<string, { readonly sha256: string }>> {
|
||||
return snapshot.bundledManifest?.files ?? {};
|
||||
if (snapshot.bundledManifest) return snapshot.bundledManifest.files;
|
||||
if (!snapshot.manifest) return {};
|
||||
const bundledPacks = Capacitor.isNativePlatform()
|
||||
? STATIC_ANDROID_BUNDLED_PACKS
|
||||
: STATIC_BROWSER_BUNDLED_PACKS;
|
||||
return Object.fromEntries(Object.entries(snapshot.manifest.files).filter(([, entry]) => (
|
||||
bundledPacks.has(entry.packId)
|
||||
)));
|
||||
}
|
||||
|
||||
function activeFileIdentities(): Readonly<Record<string, { readonly sha256: string }>> {
|
||||
@@ -117,21 +167,31 @@ export function resolveContentUrl(logicalUrl: string): string {
|
||||
return installed?.fileUri ? Capacitor.convertFileSrc(installed.fileUri) : logicalUrl;
|
||||
}
|
||||
|
||||
export function contentPackIsBundled(packId: string): boolean {
|
||||
if (!Capacitor.isNativePlatform()) return STATIC_BROWSER_BUNDLED_PACKS.has(packId);
|
||||
return STATIC_ANDROID_BUNDLED_PACKS.has(packId)
|
||||
|| Boolean(snapshot.bundledManifest?.packs.some((pack) => pack.id === packId));
|
||||
}
|
||||
|
||||
export function contentPackIsInstalled(packId: string): boolean {
|
||||
return contentPackIsBundled(packId) || Boolean(installedState.packVersions[packId]);
|
||||
}
|
||||
|
||||
export function contentPackIsUsable(packId: string): boolean {
|
||||
if (!Capacitor.isNativePlatform()) return true;
|
||||
const installed = STATIC_ANDROID_BUNDLED_PACKS.has(packId)
|
||||
|| snapshot.bundledManifest?.packs.some((pack) => pack.id === packId)
|
||||
|| Boolean(installedState.packVersions[packId]);
|
||||
if (!installed) return false;
|
||||
if (contentPackIsBundled(packId)) return true;
|
||||
if (!installedState.packVersions[packId]) return false;
|
||||
const pack = snapshot.manifest?.packs.find((candidate) => candidate.id === packId);
|
||||
return pack?.dependencies.every((dependency) => contentPackIsUsable(dependency)) ?? true;
|
||||
}
|
||||
|
||||
export function contentPackHasUpdate(packId: string): boolean {
|
||||
if (!snapshot.manifest) return false;
|
||||
if (!snapshot.manifest || !contentPackIsInstalled(packId)) return false;
|
||||
const pack = snapshot.manifest.packs.find((candidate) => candidate.id === packId);
|
||||
return changedFilesForPack(snapshot.manifest, packId, activeFileIdentities()).length > 0
|
||||
|| Boolean(pack?.dependencies.some((dependency) => contentPackHasUpdate(dependency)));
|
||||
|| Boolean(!contentPackIsBundled(packId)
|
||||
&& pack?.dependencies.some((dependency) => (
|
||||
!contentPackIsUsable(dependency) || contentPackHasUpdate(dependency)
|
||||
)));
|
||||
}
|
||||
|
||||
export async function checkForContentUpdates(): Promise<ContentManifest | null> {
|
||||
@@ -149,7 +209,7 @@ export async function checkForContentUpdates(): Promise<ContentManifest | null>
|
||||
}
|
||||
|
||||
async function loadBundledManifest(): Promise<void> {
|
||||
if (!Capacitor.isNativePlatform()) return;
|
||||
if (!Capacitor.isNativePlatform() && import.meta.env.VITE_CONTENT_MODE !== "downloadable") return;
|
||||
try {
|
||||
const response = await fetch("/content/bootstrap-manifest.json", { cache: "force-cache" });
|
||||
if (!response.ok) return;
|
||||
@@ -162,13 +222,102 @@ async function loadBundledManifest(): Promise<void> {
|
||||
export function initializeContentSystem(): Promise<void> {
|
||||
if (!initialization) {
|
||||
initialization = (async () => {
|
||||
await loadBundledManifest();
|
||||
await Promise.all([loadBundledManifest(), registerBrowserContentWorker()]);
|
||||
if (typeof navigator === "undefined" || navigator.onLine !== false) void checkForContentUpdates();
|
||||
})();
|
||||
}
|
||||
return initialization;
|
||||
}
|
||||
|
||||
async function sha256Bytes(bytes: ArrayBuffer): Promise<string> {
|
||||
if (!globalThis.crypto?.subtle) {
|
||||
throw new Error("This browser cannot verify downloaded content.");
|
||||
}
|
||||
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
||||
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
async function installBrowserContentPack(
|
||||
packId: string,
|
||||
manifest: ContentManifest,
|
||||
changes: ReturnType<typeof changedFilesForPack>,
|
||||
): Promise<void> {
|
||||
const storage = browserCacheStorage();
|
||||
if (!storage) throw new Error("Offline content storage is unavailable in this browser.");
|
||||
|
||||
const totalBytes = changes.reduce((total, [, entry]) => total + entry.bytes, 0);
|
||||
const stagingName = `${BROWSER_CONTENT_CACHE_NAME}-staging-${encodeURIComponent(packId)}`;
|
||||
await storage.delete(stagingName);
|
||||
const staging = await storage.open(stagingName);
|
||||
publish({ installingPackId: packId, downloadedBytes: 0, totalBytes, error: "" });
|
||||
|
||||
let completedBytes = 0;
|
||||
try {
|
||||
for (const [logicalPath, entry] of changes) {
|
||||
const response = await fetch(absoluteContentUrl(entry.url), { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`Content request failed (${response.status}): ${logicalPath}`);
|
||||
const bytes = await response.arrayBuffer();
|
||||
if (bytes.byteLength !== entry.bytes) {
|
||||
throw new Error(`Downloaded content has the wrong size: ${logicalPath}`);
|
||||
}
|
||||
if (await sha256Bytes(bytes) !== entry.sha256) {
|
||||
throw new Error(`Downloaded content failed verification: ${logicalPath}`);
|
||||
}
|
||||
const headers = new Headers();
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (contentType) headers.set("content-type", contentType);
|
||||
await staging.put(browserCacheRequest(logicalPath), new Response(bytes, {
|
||||
headers,
|
||||
status: 200,
|
||||
}));
|
||||
completedBytes += entry.bytes;
|
||||
publish({ downloadedBytes: completedBytes });
|
||||
}
|
||||
|
||||
const active = await storage.open(BROWSER_CONTENT_CACHE_NAME);
|
||||
for (const [logicalPath] of changes) {
|
||||
const cached = await staging.match(browserCacheRequest(logicalPath));
|
||||
if (!cached) throw new Error(`Verified content was not staged: ${logicalPath}`);
|
||||
await active.put(browserCacheRequest(logicalPath), cached);
|
||||
}
|
||||
|
||||
const pendingFiles = { ...installedState.files };
|
||||
const latestPaths = new Set(filesForContentPack(manifest, packId).map(([logicalPath]) => logicalPath));
|
||||
for (const [logicalPath, installed] of Object.entries(pendingFiles)) {
|
||||
if (installed.packId !== packId || latestPaths.has(logicalPath)) continue;
|
||||
await active.delete(browserCacheRequest(logicalPath));
|
||||
delete pendingFiles[logicalPath];
|
||||
}
|
||||
for (const [logicalPath, entry] of filesForContentPack(manifest, packId)) {
|
||||
pendingFiles[logicalPath] = {
|
||||
packId,
|
||||
sha256: entry.sha256,
|
||||
bytes: entry.bytes,
|
||||
extension: extensionForEntry(entry),
|
||||
fileUri: absoluteContentUrl(logicalPath),
|
||||
};
|
||||
}
|
||||
installedState = {
|
||||
version: 1,
|
||||
files: pendingFiles,
|
||||
packVersions: { ...installedState.packVersions, [packId]: manifest.version },
|
||||
};
|
||||
persistInstalledState();
|
||||
publish({
|
||||
installingPackId: null,
|
||||
downloadedBytes: totalBytes,
|
||||
totalBytes,
|
||||
error: "",
|
||||
installedRevision: snapshot.installedRevision + 1,
|
||||
});
|
||||
} catch (error) {
|
||||
publish({ installingPackId: null, error: error instanceof Error ? error.message : "Content download failed." });
|
||||
throw error;
|
||||
} finally {
|
||||
await storage.delete(stagingName).catch(() => false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function installContentPack(packId: string): Promise<void> {
|
||||
const manifest = snapshot.manifest ?? await checkForContentUpdates();
|
||||
if (!manifest) throw new Error(snapshot.error || "Content server is unavailable.");
|
||||
@@ -177,15 +326,20 @@ export async function installContentPack(packId: string): Promise<void> {
|
||||
}
|
||||
const pack = manifest.packs.find((candidate) => candidate.id === packId);
|
||||
if (!pack) throw new Error(`Unknown content pack: ${packId}`);
|
||||
if (!Capacitor.isNativePlatform()) return;
|
||||
|
||||
for (const dependency of pack.dependencies) {
|
||||
if (!contentPackIsUsable(dependency) || contentPackHasUpdate(dependency)) {
|
||||
await installContentPack(dependency);
|
||||
if (!contentPackIsBundled(packId)) {
|
||||
for (const dependency of pack.dependencies) {
|
||||
if (!contentPackIsUsable(dependency) || contentPackHasUpdate(dependency)) {
|
||||
await installContentPack(dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const changes = changedFilesForPack(manifest, packId, activeFileIdentities());
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
await installBrowserContentPack(packId, manifest, changes);
|
||||
return;
|
||||
}
|
||||
const totalBytes = changes.reduce((total, [, entry]) => total + entry.bytes, 0);
|
||||
const storage = await NativeContentStorage.getFreeBytes();
|
||||
const safetyReserve = 128 * 1024 * 1024;
|
||||
@@ -232,7 +386,13 @@ export async function installContentPack(packId: string): Promise<void> {
|
||||
packVersions: { ...installedState.packVersions, [packId]: manifest.version },
|
||||
};
|
||||
persistInstalledState();
|
||||
publish({ installingPackId: null, downloadedBytes: totalBytes, totalBytes, error: "" });
|
||||
publish({
|
||||
installingPackId: null,
|
||||
downloadedBytes: totalBytes,
|
||||
totalBytes,
|
||||
error: "",
|
||||
installedRevision: snapshot.installedRevision + 1,
|
||||
});
|
||||
} catch (error) {
|
||||
publish({ installingPackId: null, error: error instanceof Error ? error.message : "Content download failed." });
|
||||
throw error;
|
||||
@@ -242,19 +402,29 @@ export async function installContentPack(packId: string): Promise<void> {
|
||||
}
|
||||
|
||||
export async function removeContentPack(packId: string): Promise<void> {
|
||||
if (!Capacitor.isNativePlatform()) return;
|
||||
if (contentPackIsBundled(packId)) return;
|
||||
const remainingFiles = { ...installedState.files };
|
||||
const targetFiles = Object.entries(remainingFiles).filter(([, entry]) => entry.packId === packId);
|
||||
for (const [logicalPath, entry] of targetFiles) {
|
||||
const shared = Object.entries(remainingFiles).some(([otherPath, other]) => (
|
||||
otherPath !== logicalPath && other.sha256 === entry.sha256 && other.extension === entry.extension && other.packId !== packId
|
||||
));
|
||||
if (!shared) await NativeContentStorage.remove({ sha256: entry.sha256, extension: entry.extension });
|
||||
delete remainingFiles[logicalPath];
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
for (const [logicalPath, entry] of targetFiles) {
|
||||
const shared = Object.entries(remainingFiles).some(([otherPath, other]) => (
|
||||
otherPath !== logicalPath && other.sha256 === entry.sha256 && other.extension === entry.extension && other.packId !== packId
|
||||
));
|
||||
if (!shared) await NativeContentStorage.remove({ sha256: entry.sha256, extension: entry.extension });
|
||||
delete remainingFiles[logicalPath];
|
||||
}
|
||||
} else {
|
||||
const storage = browserCacheStorage();
|
||||
if (!storage) throw new Error("Offline content storage is unavailable in this browser.");
|
||||
const active = await storage.open(BROWSER_CONTENT_CACHE_NAME);
|
||||
for (const [logicalPath] of targetFiles) {
|
||||
await active.delete(browserCacheRequest(logicalPath));
|
||||
delete remainingFiles[logicalPath];
|
||||
}
|
||||
}
|
||||
const packVersions = { ...installedState.packVersions };
|
||||
delete packVersions[packId];
|
||||
installedState = { version: 1, files: remainingFiles, packVersions };
|
||||
persistInstalledState();
|
||||
publish({ error: "" });
|
||||
publish({ error: "", installedRevision: snapshot.installedRevision + 1 });
|
||||
}
|
||||
Reference in new issue
Block a user