Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57e18f6ce6 | ||
|
|
29369e23c1 | ||
|
|
687edb9226 | ||
|
|
ba860aacaa | ||
|
|
161dafbdb8 | ||
|
|
a382c75968 | ||
|
|
6f47a05cc2 | ||
|
|
d8d5894b28 | ||
|
|
46ae756d15 | ||
|
|
8b63799f1b | ||
|
|
60a69e31ef | ||
|
|
57ef0af4a2 | ||
|
|
597b5fbc8d | ||
|
|
5412577519 |
No files matched your search
@@ -1,3 +1,15 @@
|
|||||||
|
## [1.69.2](https://github.com/homarr-labs/homarr/compare/v1.69.1...v1.69.2) (2026-07-06)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **layout:** raise AppShellHeader z-index above Indicator dots ([687edb9](https://github.com/homarr-labs/homarr/commit/687edb9226307cb83986097abb24fe7f672e1778)), closes [#6194](https://github.com/homarr-labs/homarr/issues/6194)
|
||||||
|
|
||||||
|
## [1.69.1](https://github.com/homarr-labs/homarr/compare/v1.69.0...v1.69.1) (2026-07-04)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* unblock query cache mutations in demo mode and add cache buster ([#6176](https://github.com/homarr-labs/homarr/issues/6176)) ([a382c75](https://github.com/homarr-labs/homarr/commit/a382c75968e73bafff55b5890b1e95f0779b50d3))
|
||||||
|
|
||||||
## [1.69.0](https://github.com/homarr-labs/homarr/compare/v1.68.1...v1.69.0) (2026-07-03)
|
## [1.69.0](https://github.com/homarr-labs/homarr/compare/v1.68.1...v1.69.0) (2026-07-03)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type { AppRouter } from "@homarr/api";
|
|||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import {
|
import {
|
||||||
isPersistableWidgetQueryKey,
|
isPersistableWidgetQueryKey,
|
||||||
|
queryCacheBuster,
|
||||||
queryCacheDefaultGcTimeMs,
|
queryCacheDefaultGcTimeMs,
|
||||||
queryCacheDefaultRefetchIntervalMs,
|
queryCacheDefaultRefetchIntervalMs,
|
||||||
queryCacheDefaultStaleTimeMs,
|
queryCacheDefaultStaleTimeMs,
|
||||||
@@ -136,6 +137,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
|||||||
client={queryClient}
|
client={queryClient}
|
||||||
persistOptions={{
|
persistOptions={{
|
||||||
persister,
|
persister,
|
||||||
|
buster: queryCacheBuster,
|
||||||
maxAge: queryCacheDefaultGcTimeMs,
|
maxAge: queryCacheDefaultGcTimeMs,
|
||||||
dehydrateOptions: {
|
dehydrateOptions: {
|
||||||
shouldDehydrateQuery: (query) =>
|
shouldDehydrateQuery: (query) =>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import "~/styles/gridstack.scss";
|
|||||||
|
|
||||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||||
import type { PersistedClient } from "@tanstack/react-query-persist-client";
|
import type { PersistedClient } from "@tanstack/react-query-persist-client";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { queryCacheBuster } from "@homarr/api/query-cache";
|
||||||
import { getQueryClient } from "@homarr/api/server";
|
import { getQueryClient } from "@homarr/api/server";
|
||||||
import { IntegrationProvider } from "@homarr/auth/client";
|
import { IntegrationProvider } from "@homarr/auth/client";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
@@ -121,7 +123,8 @@ async function QueryCacheHydration({ userId, boardId }: { userId: string; boardI
|
|||||||
const serialized = await getQueryCacheAsync(userId, boardId);
|
const serialized = await getQueryCacheAsync(userId, boardId);
|
||||||
if (!serialized) return null;
|
if (!serialized) return null;
|
||||||
|
|
||||||
const persisted = JSON.parse(serialized) as PersistedClient;
|
const persisted = superjson.parse<PersistedClient>(serialized);
|
||||||
|
if (persisted.buster !== queryCacheBuster) return null;
|
||||||
if (!persisted?.clientState?.queries?.length) return null;
|
if (!persisted?.clientState?.queries?.length) return null;
|
||||||
|
|
||||||
return <HydrationBoundary state={persisted.clientState} />;
|
return <HydrationBoundary state={persisted.clientState} />;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface Props {
|
|||||||
|
|
||||||
export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
|
export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
|
||||||
return (
|
return (
|
||||||
<AppShellHeader maw="100vw" style={{ overflowX: "hidden" }}>
|
<AppShellHeader maw="100vw" zIndex={201} style={{ overflowX: "hidden" }}>
|
||||||
<Group h="100%" gap="xl" px="md" justify="apart" wrap="nowrap">
|
<Group h="100%" gap="xl" px="md" justify="apart" wrap="nowrap">
|
||||||
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
|
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
|
||||||
{hasNavigation && <ClientBurger />}
|
{hasNavigation && <ClientBurger />}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.69.0",
|
"version": "1.69.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const queryCacheDefaultRefetchIntervalMs = 30_000;
|
|||||||
export const queryCacheDefaultGcTimeMs = 1000 * 60 * 60 * 24;
|
export const queryCacheDefaultGcTimeMs = 1000 * 60 * 60 * 24;
|
||||||
export const queryCacheMaxValueBytes = 1024 * 1024;
|
export const queryCacheMaxValueBytes = 1024 * 1024;
|
||||||
export const queryCacheStoragePrefix = "homarr-widget-query";
|
export const queryCacheStoragePrefix = "homarr-widget-query";
|
||||||
|
export const queryCacheBuster = "v2-superjson";
|
||||||
|
|
||||||
let activeQueryCacheBoardId: string | null = null;
|
let activeQueryCacheBoardId: string | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { boards } from "@homarr/db/schema";
|
|||||||
import { removeQueryCacheAsync, setQueryCacheAsync } from "@homarr/redis";
|
import { removeQueryCacheAsync, setQueryCacheAsync } from "@homarr/redis";
|
||||||
|
|
||||||
import { queryCacheDefaultGcTimeMs, queryCacheMaxValueBytes } from "../query-cache";
|
import { queryCacheDefaultGcTimeMs, queryCacheMaxValueBytes } from "../query-cache";
|
||||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
import { createTRPCRouter, internalProcedure } from "../trpc";
|
||||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||||
|
|
||||||
const queryCacheBoardInput = z.object({
|
const queryCacheBoardInput = z.object({
|
||||||
@@ -21,7 +21,7 @@ const validateBoardAccessAsync = async (ctx: Parameters<typeof throwIfActionForb
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const queryCacheRouter = createTRPCRouter({
|
export const queryCacheRouter = createTRPCRouter({
|
||||||
setItem: publicProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => {
|
setItem: internalProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => {
|
||||||
await validateBoardAccessAsync(ctx, input.boardId);
|
await validateBoardAccessAsync(ctx, input.boardId);
|
||||||
const userId = ctx.session?.user.id ?? "anonymous";
|
const userId = ctx.session?.user.id ?? "anonymous";
|
||||||
return {
|
return {
|
||||||
@@ -34,7 +34,7 @@ export const queryCacheRouter = createTRPCRouter({
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
removeItem: publicProcedure.input(queryCacheBoardInput).mutation(async ({ ctx, input }) => {
|
removeItem: internalProcedure.input(queryCacheBoardInput).mutation(async ({ ctx, input }) => {
|
||||||
await validateBoardAccessAsync(ctx, input.boardId);
|
await validateBoardAccessAsync(ctx, input.boardId);
|
||||||
const userId = ctx.session?.user.id ?? "anonymous";
|
const userId = ctx.session?.user.id ?? "anonymous";
|
||||||
await removeQueryCacheAsync(userId, input.boardId);
|
await removeQueryCacheAsync(userId, input.boardId);
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ const baseProcedure = env.DEMO_MODE ? t.procedure.use(enforceDemoModeReadOnly) :
|
|||||||
*/
|
*/
|
||||||
export const publicProcedure = baseProcedure;
|
export const publicProcedure = baseProcedure;
|
||||||
|
|
||||||
|
export const internalProcedure = t.procedure;
|
||||||
|
|
||||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||||
if (!ctx.session?.user) {
|
if (!ctx.session?.user) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ interface Options<TData, TInput extends Record<string, unknown>> {
|
|||||||
type CacheEntry<TData> = { data: TData; timestamp: Date; expiresAt: number };
|
type CacheEntry<TData> = { data: TData; timestamp: Date; expiresAt: number };
|
||||||
|
|
||||||
const MAX_CACHE_SIZE = 1000;
|
const MAX_CACHE_SIZE = 1000;
|
||||||
const cache = new Map<string, CacheEntry<unknown>>();
|
|
||||||
const inflight = new Map<string, Promise<CacheEntry<unknown>>>();
|
|
||||||
|
|
||||||
const DEFAULT_TTL_MS = 10_000;
|
const DEFAULT_TTL_MS = 10_000;
|
||||||
|
|
||||||
const evictExpired = () => {
|
const evictExpired = <TData>(cache: Map<string, CacheEntry<TData>>) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [key, entry] of cache) {
|
for (const [key, entry] of cache) {
|
||||||
if (now >= entry.expiresAt) cache.delete(key);
|
if (now >= entry.expiresAt) cache.delete(key);
|
||||||
@@ -20,41 +17,46 @@ const evictExpired = () => {
|
|||||||
|
|
||||||
export const createRequestHandler = <TData, TInput extends Record<string, unknown>>(
|
export const createRequestHandler = <TData, TInput extends Record<string, unknown>>(
|
||||||
options: Options<TData, TInput>,
|
options: Options<TData, TInput>,
|
||||||
) => ({
|
) => {
|
||||||
handler: (input: TInput) => ({
|
const cache = new Map<string, CacheEntry<TData>>();
|
||||||
async getDataAsync(): Promise<{ data: TData; timestamp: Date }> {
|
const inflight = new Map<string, Promise<CacheEntry<TData>>>();
|
||||||
const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
|
|
||||||
const key = JSON.stringify(input);
|
|
||||||
|
|
||||||
const cached = cache.get(key) as CacheEntry<TData> | undefined;
|
return {
|
||||||
if (cached && Date.now() < cached.expiresAt) {
|
handler: (input: TInput) => ({
|
||||||
return { data: cached.data, timestamp: cached.timestamp };
|
async getDataAsync(): Promise<{ data: TData; timestamp: Date }> {
|
||||||
}
|
const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
|
||||||
if (cached) cache.delete(key);
|
const key = JSON.stringify(input);
|
||||||
|
|
||||||
const existing = inflight.get(key);
|
const cached = cache.get(key);
|
||||||
if (existing) return existing as Promise<CacheEntry<TData>>;
|
if (cached && Date.now() < cached.expiresAt) {
|
||||||
|
return { data: cached.data, timestamp: cached.timestamp };
|
||||||
|
}
|
||||||
|
if (cached) cache.delete(key);
|
||||||
|
|
||||||
const promise = options
|
const existing = inflight.get(key);
|
||||||
.requestAsync(input)
|
if (existing) return existing;
|
||||||
.then((data) => {
|
|
||||||
if (cache.size >= MAX_CACHE_SIZE) evictExpired();
|
|
||||||
if (cache.size >= MAX_CACHE_SIZE) {
|
|
||||||
const oldest = cache.keys().next().value;
|
|
||||||
if (oldest) cache.delete(oldest);
|
|
||||||
}
|
|
||||||
const entry: CacheEntry<TData> = { data, timestamp: new Date(), expiresAt: Date.now() + ttl };
|
|
||||||
cache.set(key, entry as CacheEntry<unknown>);
|
|
||||||
inflight.delete(key);
|
|
||||||
return entry;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
inflight.delete(key);
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
inflight.set(key, promise as Promise<CacheEntry<unknown>>);
|
const promise = options
|
||||||
return promise;
|
.requestAsync(input)
|
||||||
},
|
.then((data) => {
|
||||||
}),
|
if (cache.size >= MAX_CACHE_SIZE) evictExpired(cache);
|
||||||
});
|
if (cache.size >= MAX_CACHE_SIZE) {
|
||||||
|
const oldest = cache.keys().next().value;
|
||||||
|
if (oldest) cache.delete(oldest);
|
||||||
|
}
|
||||||
|
const entry: CacheEntry<TData> = { data, timestamp: new Date(), expiresAt: Date.now() + ttl };
|
||||||
|
cache.set(key, entry);
|
||||||
|
inflight.delete(key);
|
||||||
|
return entry;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
inflight.delete(key);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
inflight.set(key, promise);
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in new issue
Block a user