From 920cd92c163fa800b67e034e5fcf8926af9d6bc5 Mon Sep 17 00:00:00 2001
From: ddCat <2695092045@qq.com>
Date: Fri, 14 Aug 2026 18:35:17 +0800
Subject: [PATCH] =?UTF-8?q?perf(platform):=20=E5=88=9B=E4=BD=9C=E9=A1=B5?=
=?UTF-8?q?=E5=8A=A0=E8=BD=BD=20-=20=E5=90=88=E5=B9=B6=E7=94=A8=E6=88=B7?=
=?UTF-8?q?=E6=95=B0=E6=8D=AE=E4=B8=8E=E4=BD=99=E9=A2=9D=E8=AF=B7=E6=B1=82?=
=?UTF-8?q?=E5=B9=B6=E6=8C=89=E8=B7=AF=E7=94=B1=E5=BB=B6=E8=BF=9F=E5=8A=A0?=
=?UTF-8?q?=E8=BD=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/internal/handler/user_data.go | 13 +++
backend/internal/service/user_data.go | 17 +++
web/src/components/layout/app-top-nav.tsx | 1 +
web/src/hooks/use-wallet-balance.ts | 88 +++++----------
web/src/router.tsx | 130 ++++++++++++----------
web/src/services/api/user-data.ts | 8 ++
web/src/services/file-storage.ts | 7 +-
web/src/services/image-storage.ts | 7 +-
web/src/services/user-data-sync.ts | 37 +++---
9 files changed, 160 insertions(+), 148 deletions(-)
diff --git a/backend/internal/handler/user_data.go b/backend/internal/handler/user_data.go
index d9b326aa9..4c88973d5 100644
--- a/backend/internal/handler/user_data.go
+++ b/backend/internal/handler/user_data.go
@@ -299,6 +299,19 @@ func RegisterUserDataRoutes(r *gin.RouterGroup, svc *service.Service) {
}
ok(c, gin.H{"assets": assets})
})
+ r.GET("/user-data/snapshot", func(c *gin.Context) {
+ user, err := currentUser(c, svc)
+ if err != nil {
+ failService(c, err)
+ return
+ }
+ snapshot, err := svc.UserDataSnapshot(user.ID)
+ if err != nil {
+ failService(c, err)
+ return
+ }
+ ok(c, snapshot)
+ })
r.GET("/assets/:id", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
diff --git a/backend/internal/service/user_data.go b/backend/internal/service/user_data.go
index d539bf52b..17b22facd 100644
--- a/backend/internal/service/user_data.go
+++ b/backend/internal/service/user_data.go
@@ -29,6 +29,23 @@ type UserDataSummary struct {
UpdatedAt time.Time `json:"updatedAt"`
}
+type UserDataSnapshot struct {
+ Assets []json.RawMessage `json:"assets"`
+ Projects []json.RawMessage `json:"projects"`
+}
+
+func (s *Service) UserDataSnapshot(userID string) (UserDataSnapshot, error) {
+ assets, err := s.UserAssets(userID)
+ if err != nil {
+ return UserDataSnapshot{}, err
+ }
+ projects, err := s.UserCanvasProjects(userID)
+ if err != nil {
+ return UserDataSnapshot{}, err
+ }
+ return UserDataSnapshot{Assets: assets, Projects: projects}, nil
+}
+
func (s *Service) UserAssetSummaries(userID string) ([]UserDataSummary, error) {
assets, err := s.repo.AssetSummaries(userID)
if err != nil {
diff --git a/web/src/components/layout/app-top-nav.tsx b/web/src/components/layout/app-top-nav.tsx
index f846b86ea..f20799198 100644
--- a/web/src/components/layout/app-top-nav.tsx
+++ b/web/src/components/layout/app-top-nav.tsx
@@ -178,6 +178,7 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
["wallet-balance", userId] as const;
export function useWalletBalance(userId?: string, enabled = true) {
const activeUserId = enabled ? userId || "" : "";
- const requestSequence = useRef(0);
- const [snapshot, setSnapshot] = useState({ userId: "", availableMicrocredits: null, refreshing: false });
-
- const refresh = useCallback(async () => {
- const requestedUserId = activeUserId;
- const sequence = ++requestSequence.current;
- if (!requestedUserId) {
- setSnapshot({ userId: "", availableMicrocredits: null, refreshing: false });
- return;
- }
- setSnapshot((current) => ({
- userId: requestedUserId,
- availableMicrocredits: current.userId === requestedUserId ? current.availableMicrocredits : null,
- refreshing: true,
- }));
- try {
+ const queryClient = useQueryClient();
+ const queryKey = walletBalanceQueryKey(activeUserId);
+ const query = useQuery({
+ queryKey,
+ enabled: Boolean(activeUserId),
+ queryFn: async () => {
const wallet = await getWallet(1, 1);
- if (sequence !== requestSequence.current) return;
- if (wallet.account.userId !== requestedUserId) throw new Error("积分账户与当前用户不一致");
- setSnapshot({ userId: requestedUserId, availableMicrocredits: wallet.account.availableMicrocredits, refreshing: false });
- } catch (error) {
- if (sequence !== requestSequence.current) return;
- console.warn("积分余额刷新失败", error);
- setSnapshot((current) => ({
- userId: requestedUserId,
- availableMicrocredits: current.userId === requestedUserId ? current.availableMicrocredits : null,
- refreshing: false,
- }));
- }
- }, [activeUserId]);
+ if (wallet.account.userId !== activeUserId) throw new Error("积分账户与当前用户不一致");
+ return wallet.account.availableMicrocredits;
+ },
+ staleTime: 15_000,
+ refetchInterval: WALLET_REFRESH_INTERVAL_MS,
+ refetchOnWindowFocus: true,
+ });
useEffect(() => {
- if (!activeUserId) {
- requestSequence.current += 1;
- setSnapshot({ userId: "", availableMicrocredits: null, refreshing: false });
- return;
- }
- void refresh();
- const timer = window.setInterval(() => void refresh(), WALLET_REFRESH_INTERVAL_MS);
- const handleFocus = () => void refresh();
- const handleVisibility = () => {
- if (document.visibilityState === "visible") void refresh();
- };
- window.addEventListener("focus", handleFocus);
- window.addEventListener("wallet:updated", handleFocus);
- document.addEventListener("visibilitychange", handleVisibility);
+ if (!activeUserId) return;
+ const handleUpdated = () => void queryClient.invalidateQueries({ queryKey: walletBalanceQueryKey(activeUserId) });
+ window.addEventListener("wallet:updated", handleUpdated);
return () => {
- requestSequence.current += 1;
- window.clearInterval(timer);
- window.removeEventListener("focus", handleFocus);
- window.removeEventListener("wallet:updated", handleFocus);
- document.removeEventListener("visibilitychange", handleVisibility);
+ window.removeEventListener("wallet:updated", handleUpdated);
};
- }, [activeUserId, refresh]);
+ }, [activeUserId, queryClient]);
+
+ useEffect(() => {
+ if (query.error) console.warn("积分余额刷新失败", query.error);
+ }, [query.error]);
- const snapshotMatchesUser = snapshot.userId === activeUserId;
return {
- availableMicrocredits: snapshotMatchesUser ? snapshot.availableMicrocredits : null,
- refreshing: snapshotMatchesUser ? snapshot.refreshing : Boolean(activeUserId),
- refresh,
+ availableMicrocredits: query.data ?? null,
+ refreshing: query.isFetching,
+ refresh: query.refetch,
};
}
diff --git a/web/src/router.tsx b/web/src/router.tsx
index 00849c9be..e9555ba6f 100644
--- a/web/src/router.tsx
+++ b/web/src/router.tsx
@@ -1,47 +1,59 @@
+import { lazy, Suspense, type ReactNode } from "react";
import { createBrowserRouter, Navigate, Outlet } from "react-router";
import { RequireAuth } from "@/components/auth/require-auth";
import { RequireFeature } from "@/components/auth/require-feature";
+import { FullScreenLoader } from "@/components/ui/aceternity/full-screen-loader";
import UserLayout from "@/layouts/user-layout";
-import AdminPage from "@/pages/admin";
-import { AccessSettingsPage, AnalyticsPage, AnnouncementsPage, CreditOperationsPage, EmailSettingsPage, FeatureAvailabilityPage } from "@/pages/admin/admin-route-pages";
-import ChannelsPage from "@/pages/admin/channels/channels-page";
-import LogsPage from "@/pages/admin/logs/logs-page";
-import RedemptionCodesPage from "@/pages/admin/redemption-codes/redemption-codes-page";
-import RuntimePolicySettingsPage from "@/pages/admin/settings/runtime-policy-settings-page";
-import DrawingEngineSettingsPage from "@/pages/admin/settings/drawing-engine-settings-page";
-import StorageSettingsPage from "@/pages/admin/settings/storage-settings-page";
-import StoryboardPromptsPage from "@/pages/admin/storyboard-prompts/storyboard-prompts-page";
-import UsersPage from "@/pages/admin/users/users-page";
-import AssetsPage from "@/pages/assets";
import { AuthScene } from "@/pages/auth/auth-scene";
-import LoginPage from "@/pages/auth/login";
-import RegisterPage from "@/pages/auth/register";
-import CanvasPage from "@/pages/canvas";
-import CanvasProjectPage from "@/pages/canvas/project";
-import SharedCanvasPage from "@/pages/canvas/shared";
-import CreatePage from "@/pages/create";
-import HomePage from "@/pages/home";
-import NotFound from "@/pages/not-found";
import RouteErrorPage from "@/pages/route-error";
-import SkillsPage from "@/pages/skills";
-import TasksPage from "@/pages/tasks";
-import WalletPage from "@/pages/wallet";
-import ProjectsPage from "@/pages/projects";
-import ProjectDetailPage from "@/pages/projects/detail";
-import SettingsPage from "@/pages/settings";
-import TestVoiceRecording from "@/pages/test-voice-recording";
+
+const AdminPage = lazy(() => import("@/pages/admin"));
+const AnalyticsPage = lazy(() => import("@/pages/admin/admin-route-pages").then((module) => ({ default: module.AnalyticsPage })));
+const AnnouncementsPage = lazy(() => import("@/pages/admin/admin-route-pages").then((module) => ({ default: module.AnnouncementsPage })));
+const CreditOperationsPage = lazy(() => import("@/pages/admin/admin-route-pages").then((module) => ({ default: module.CreditOperationsPage })));
+const AccessSettingsPage = lazy(() => import("@/pages/admin/admin-route-pages").then((module) => ({ default: module.AccessSettingsPage })));
+const EmailSettingsPage = lazy(() => import("@/pages/admin/admin-route-pages").then((module) => ({ default: module.EmailSettingsPage })));
+const FeatureAvailabilityPage = lazy(() => import("@/pages/admin/admin-route-pages").then((module) => ({ default: module.FeatureAvailabilityPage })));
+const ChannelsPage = lazy(() => import("@/pages/admin/channels/channels-page"));
+const LogsPage = lazy(() => import("@/pages/admin/logs/logs-page"));
+const RedemptionCodesPage = lazy(() => import("@/pages/admin/redemption-codes/redemption-codes-page"));
+const RuntimePolicySettingsPage = lazy(() => import("@/pages/admin/settings/runtime-policy-settings-page"));
+const DrawingEngineSettingsPage = lazy(() => import("@/pages/admin/settings/drawing-engine-settings-page"));
+const StorageSettingsPage = lazy(() => import("@/pages/admin/settings/storage-settings-page"));
+const StoryboardPromptsPage = lazy(() => import("@/pages/admin/storyboard-prompts/storyboard-prompts-page"));
+const UsersPage = lazy(() => import("@/pages/admin/users/users-page"));
+const AssetsPage = lazy(() => import("@/pages/assets"));
+const LoginPage = lazy(() => import("@/pages/auth/login"));
+const RegisterPage = lazy(() => import("@/pages/auth/register"));
+const CanvasPage = lazy(() => import("@/pages/canvas"));
+const CanvasProjectPage = lazy(() => import("@/pages/canvas/project"));
+const SharedCanvasPage = lazy(() => import("@/pages/canvas/shared"));
+const CreatePage = lazy(() => import("@/pages/create"));
+const HomePage = lazy(() => import("@/pages/home"));
+const NotFound = lazy(() => import("@/pages/not-found"));
+const SkillsPage = lazy(() => import("@/pages/skills"));
+const TasksPage = lazy(() => import("@/pages/tasks"));
+const WalletPage = lazy(() => import("@/pages/wallet"));
+const ProjectsPage = lazy(() => import("@/pages/projects"));
+const ProjectDetailPage = lazy(() => import("@/pages/projects/detail"));
+const SettingsPage = lazy(() => import("@/pages/settings"));
+const TestVoiceRecording = lazy(() => import("@/pages/test-voice-recording"));
+
+function deferred(element: ReactNode) {
+ return }>{element};
+}
export const router = createBrowserRouter([
{
element: ,
errorElement: ,
children: [
- { path: "/login", element: },
- { path: "/register", element: },
+ { path: "/login", element: deferred() },
+ { path: "/register", element: deferred() },
],
},
- { path: "/share/canvas/:token", element: , errorElement: },
+ { path: "/share/canvas/:token", element: deferred(), errorElement: },
{
element: (
@@ -51,44 +63,44 @@ export const router = createBrowserRouter([
errorElement: ,
children: [
{ path: "/", element: },
- { path: "/create", element: },
- { path: "/home", element: },
- { path: "/tasks", element: },
- { path: "/assets", element: },
- { path: "/skills", element: },
- { path: "/wallet", element: },
- { path: "/settings", element: },
- { path: "/test-voice-recording", element: },
- { path: "/projects", element: },
- { path: "/projects/:projectId", element: },
- { path: "/projects/:projectId/:view", element: },
- { path: "/projects/:projectId/chapters/:chapterId", element: },
- { path: "/canvas", element: },
- { path: "/canvas/:id", element: },
+ { path: "/create", element: {deferred()} },
+ { path: "/home", element: deferred() },
+ { path: "/tasks", element: {deferred()} },
+ { path: "/assets", element: {deferred()} },
+ { path: "/skills", element: {deferred()} },
+ { path: "/wallet", element: {deferred()} },
+ { path: "/settings", element: {deferred()} },
+ { path: "/test-voice-recording", element: {deferred()} },
+ { path: "/projects", element: {deferred()} },
+ { path: "/projects/:projectId", element: {deferred()} },
+ { path: "/projects/:projectId/:view", element: {deferred()} },
+ { path: "/projects/:projectId/chapters/:chapterId", element: {deferred()} },
+ { path: "/canvas", element: {deferred()} },
+ { path: "/canvas/:id", element: {deferred()} },
{
path: "/admin",
- element: ,
+ element: {deferred()},
children: [
- { index: true, element: },
- { path: "users", element: },
- { path: "channels", element: },
- { path: "prompt-templates", element: },
+ { index: true, element: deferred() },
+ { path: "users", element: deferred() },
+ { path: "channels", element: deferred() },
+ { path: "prompt-templates", element: deferred() },
{ path: "storyboard-prompts", element: },
- { path: "announcements", element: },
- { path: "credit-operations", element: },
- { path: "redemption-codes", element: },
- { path: "logs", element: },
+ { path: "announcements", element: deferred() },
+ { path: "credit-operations", element: deferred() },
+ { path: "redemption-codes", element: deferred() },
+ { path: "logs", element: deferred() },
{ path: "settings", element: },
- { path: "settings/drawing-engine", element: },
+ { path: "settings/drawing-engine", element: deferred() },
{ path: "settings/concurrency", element: },
- { path: "settings/runtime-policy", element: },
- { path: "settings/features", element: },
- { path: "settings/access", element: },
- { path: "settings/email", element: },
- { path: "settings/storage", element: },
+ { path: "settings/runtime-policy", element: deferred() },
+ { path: "settings/features", element: deferred() },
+ { path: "settings/access", element: deferred() },
+ { path: "settings/email", element: deferred() },
+ { path: "settings/storage", element: deferred() },
],
},
],
},
- { path: "*", element: },
+ { path: "*", element: deferred() },
]);
diff --git a/web/src/services/api/user-data.ts b/web/src/services/api/user-data.ts
index 7ac4c8be2..80ca28d34 100644
--- a/web/src/services/api/user-data.ts
+++ b/web/src/services/api/user-data.ts
@@ -12,6 +12,14 @@ export type RemoteUserDataSummary = {
updatedAt: string;
};
+export type RemoteUserDataSnapshot = {
+ assets: Asset[];
+ projects: CanvasProject[];
+};
+
+export function getRemoteUserDataSnapshot() {
+ return request(api.get("/user-data/snapshot"));
+}
export function listRemoteAssets() {
return request<{ assets: RemoteUserDataSummary[] }>(api.get("/assets"));
diff --git a/web/src/services/file-storage.ts b/web/src/services/file-storage.ts
index 5d916779e..5a6ec9cc9 100644
--- a/web/src/services/file-storage.ts
+++ b/web/src/services/file-storage.ts
@@ -2,7 +2,7 @@ import localforage from "localforage";
import { nanoid } from "nanoid";
import { getActiveUserScope } from "@/lib/user-scope";
-import { resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey, resolveResourceUrl, uploadResourceFile } from "@/services/api/resources";
+import { resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey, uploadResourceFile } from "@/services/api/resources";
import { getCachedResourceBlob, getCachedResourceObjectUrl, primeResourceBlobCache } from "@/services/resource-blob-cache";
export type UploadedFile = { url: string; storageKey: string; bytes: number; mimeType: string; width?: number; height?: number; durationMs?: number };
@@ -32,9 +32,10 @@ export async function uploadMediaFile(input: string | Blob, prefix = "file"): Pr
export async function resolveMediaUrl(storageKey?: string, fallback = "") {
if (!storageKey) return fallback;
- if (resourceIdFromStorageKey(storageKey)) {
+ const resourceId = resourceIdFromStorageKey(storageKey);
+ if (resourceId) {
const cached = await getCachedResourceObjectUrl(storageKey).catch(() => "");
- return cached || resolveResourceUrl(storageKey, fallback);
+ return cached || resourceFileUrl(resourceId);
}
const cached = objectUrls.get(storageKey);
if (cached) return cached;
diff --git a/web/src/services/image-storage.ts b/web/src/services/image-storage.ts
index 1951ef293..77c255d50 100644
--- a/web/src/services/image-storage.ts
+++ b/web/src/services/image-storage.ts
@@ -3,7 +3,7 @@ import localforage from "localforage";
import { nanoid } from "nanoid";
import { readImageMeta } from "@/lib/image-utils";
import { getActiveUserScope } from "@/lib/user-scope";
-import { importResourceFromUrl, isResourceUrl, resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey, resolveResourceUrl, uploadResourceFile } from "@/services/api/resources";
+import { importResourceFromUrl, isResourceUrl, resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey, uploadResourceFile } from "@/services/api/resources";
import { cacheResourceObjectUrl, getCachedResourceBlob, getCachedResourceObjectUrl, primeResourceBlobCache } from "@/services/resource-blob-cache";
export type UploadedImage = {
@@ -65,14 +65,15 @@ function shouldImportRemoteImage(input: string) {
export async function resolveImageUrl(storageKey?: string, fallback = "", options?: { cacheMiss?: boolean }) {
if (!storageKey) return fallback;
- if (resourceIdFromStorageKey(storageKey)) {
+ const resourceId = resourceIdFromStorageKey(storageKey);
+ if (resourceId) {
const cached = await getCachedResourceObjectUrl(storageKey).catch(() => "");
if (cached) return cached;
if (options?.cacheMiss) {
const populated = await cacheResourceObjectUrl(storageKey).catch(() => "");
if (populated) return populated;
}
- return resolveResourceUrl(storageKey, fallback);
+ return resourceFileUrl(resourceId);
}
const cached = objectUrls.get(storageKey);
if (cached) return cached;
diff --git a/web/src/services/user-data-sync.ts b/web/src/services/user-data-sync.ts
index d8d007afd..545e4c339 100644
--- a/web/src/services/user-data-sync.ts
+++ b/web/src/services/user-data-sync.ts
@@ -1,6 +1,6 @@
import { getMediaBlob } from "@/services/file-storage";
import { getImageBlob, resolveImageUrl } from "@/services/image-storage";
-import { deleteRemoteAsset, deleteRemoteCanvasProject, getRemoteAsset, getRemoteCanvasProject, listRemoteAssets, listRemoteCanvasProjects, upsertRemoteAsset, upsertRemoteCanvasProject, type RemoteUserDataSummary } from "@/services/api/user-data";
+import { deleteRemoteAsset, deleteRemoteCanvasProject, getRemoteUserDataSnapshot, upsertRemoteAsset, upsertRemoteCanvasProject } from "@/services/api/user-data";
import { resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey, uploadResourceFile } from "@/services/api/resources";
import type { Asset } from "@/stores/use-asset-store";
import { useAssetStore } from "@/stores/use-asset-store";
@@ -23,17 +23,13 @@ export async function syncRemoteUserData(userId?: string | null) {
if (!activeRemoteUserId) return;
applyingRemoteState = true;
try {
- const [remoteCanvas, remoteAssets] = await Promise.all([listRemoteCanvasProjects(), listRemoteAssets()]);
- remoteProjectVersions = versionMap(remoteCanvas.projects);
- remoteAssetVersions = versionMap(remoteAssets.assets);
+ const remoteSnapshot = await getRemoteUserDataSnapshot();
+ remoteProjectVersions = versionMap(remoteSnapshot.projects);
+ remoteAssetVersions = versionMap(remoteSnapshot.assets);
const localProjects = useCanvasStore.getState().projects;
const localAssets = useAssetStore.getState().assets;
- const [changedProjects, changedAssets] = await Promise.all([
- fetchNewerRemoteItems(localProjects, remoteCanvas.projects, async (id) => (await getRemoteCanvasProject(id)).project),
- fetchNewerRemoteItems(localAssets, remoteAssets.assets, async (id) => (await getRemoteAsset(id)).asset),
- ]);
- const mergedProjects = mergeById(localProjects, changedProjects);
- const mergedAssets = mergeById(localAssets, await hydrateAssets(changedAssets));
+ const mergedProjects = mergeById(localProjects, remoteSnapshot.projects);
+ const mergedAssets = mergeById(localAssets, await hydrateAssets(remoteSnapshot.assets));
useCanvasStore.getState().replaceProjects(mergedProjects);
useAssetStore.getState().replaceAssets(mergedAssets);
} finally {
@@ -128,8 +124,8 @@ async function saveRemoteUserDataBatch() {
try {
const currentProjects = useCanvasStore.getState().projects;
const currentAssets = useAssetStore.getState().assets;
- const dirtyProjects = currentProjects.filter((item) => remoteProjectVersions.get(item.id) !== item.updatedAt);
- const dirtyAssets = currentAssets.filter((item) => remoteAssetVersions.get(item.id) !== item.updatedAt);
+ const dirtyProjects = currentProjects.filter((item) => !sameVersion(remoteProjectVersions.get(item.id), item.updatedAt));
+ const dirtyAssets = currentAssets.filter((item) => !sameVersion(remoteAssetVersions.get(item.id), item.updatedAt));
const deletedProjectIds = missingIds(remoteProjectVersions, currentProjects);
const deletedAssetIds = missingIds(remoteAssetVersions, currentAssets);
if (!dirtyProjects.length && !dirtyAssets.length && !deletedProjectIds.length && !deletedAssetIds.length) return;
@@ -278,17 +274,8 @@ function mergeById(local: T[], re
return Array.from(items.values()).sort((a, b) => timeValue(b.updatedAt) - timeValue(a.updatedAt));
}
-async function fetchNewerRemoteItems(local: T[], remote: RemoteUserDataSummary[], fetchItem: (id: string) => Promise) {
- const localById = new Map(local.map((item) => [item.id, item]));
- const pending = remote.filter((item) => {
- const current = localById.get(item.id);
- return !current || timeValue(item.updatedAt) > timeValue(current.updatedAt);
- });
- return Promise.all(pending.map((item) => fetchItem(item.id)));
-}
-
-function versionMap(items: RemoteUserDataSummary[]) {
- return new Map(items.map((item) => [item.id, item.updatedAt]));
+function versionMap(items: Array<{ id: string; updatedAt?: string }>) {
+ return new Map(items.map((item) => [item.id, item.updatedAt || ""]));
}
function missingIds(remote: Map, local: T[]) {
@@ -306,6 +293,10 @@ function timeValue(value?: string) {
return Number.isFinite(time) ? time : 0;
}
+function sameVersion(remote?: string, local?: string) {
+ return Boolean(remote && local) && timeValue(remote) === timeValue(local);
+}
+
function isLocalStorageKey(value: string) {
return LOCAL_STORAGE_KEY_PATTERN.test(value) && !resourceIdFromStorageKey(value);
}