Skip to content

Commit

Permalink
feat: Base Cache Module (#132)
Browse files Browse the repository at this point in the history
* chore: added middleware cache impl

* chore: integrated entity changed function

* chore: added cache provider for some transformation population calls

* chore: added flyouts for some buttons

* chore: updated redirect
  • Loading branch information
Bendomey authored Dec 15, 2024
1 parent 0ecff78 commit 43c7496
Show file tree
Hide file tree
Showing 24 changed files with 502 additions and 125 deletions.
71 changes: 58 additions & 13 deletions apps/client/app/components/Content/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,68 @@ export const Content = ({ content, showCreator = true }: Props) => {
<div className="flex flex-row items-center justify-between p-2">
<div className="flex items-center gap-1">
{content.amount > 0 ? (
<div className="rounded-full bg-black px-3 py-1 text-sm font-semibold text-white">
mfoni+
</div>
<FlyoutContainer
intendedPosition="y"
FlyoutContent={
<div className="z-50 flex w-48 flex-col items-center justify-center rounded-2xl bg-black px-3 py-4 shadow-xl">
<h3 className="text-center text-sm font-bold text-white">
This is a premium image
</h3>
</div>
}
>
<div className="rounded-full bg-black px-3 py-1 text-sm font-semibold text-white">
mfoni+
</div>
</FlyoutContainer>
) : null}
{content.status === 'REJECTED' ? (
<div className="gap-.5 flex items-center rounded-full bg-red-600 px-2 py-1 text-xs font-semibold text-white">
<XCircleIcon className="size-5" />
Rejected
</div>
<FlyoutContainer
intendedPosition="y"
FlyoutContent={
<div className="z-50 flex w-48 flex-col items-center justify-center rounded-2xl bg-black px-3 py-4 shadow-xl">
<h3 className="text-center text-sm font-bold text-white">
{content.imageProcessingResponse?.message ??
'Something happened while processing your image.'}
</h3>
</div>
}
>
<div className="gap-.5 flex items-center rounded-full bg-red-600 px-2 py-1 text-xs font-semibold text-white">
<XCircleIcon className="size-5" />
Rejected
</div>
</FlyoutContainer>
) : content.status === 'PROCESSING' ? (
<div className="rounded-full bg-blue-600 px-1 py-1 text-sm font-semibold text-white">
<ClockIcon className="size-5" />
</div>
<FlyoutContainer
intendedPosition="y"
FlyoutContent={
<div className="z-50 flex w-48 flex-col items-center justify-center rounded-2xl bg-black px-3 py-4 shadow-xl">
<h3 className="text-center text-sm font-bold text-white">
Processing image
</h3>
</div>
}
>
<div className="rounded-full bg-blue-600 px-1 py-1 text-sm font-semibold text-white">
<ClockIcon className="size-5" />
</div>
</FlyoutContainer>
) : content.visibility === 'PRIVATE' ? (
<div className="rounded-full bg-black px-3 py-1 text-sm font-semibold text-white">
Hidden
</div>
<FlyoutContainer
intendedPosition="y"
FlyoutContent={
<div className="z-50 flex w-48 flex-col items-center justify-center rounded-2xl bg-black px-3 py-4 shadow-xl">
<h3 className="text-center text-sm font-bold text-white">
This is image is hidden from the public
</h3>
</div>
}
>
<div className="rounded-full bg-black px-3 py-1 text-sm font-semibold text-white">
Hidden
</div>
</FlyoutContainer>
) : null}
</div>
<div className="hidden group-hover:block">
Expand Down
3 changes: 3 additions & 0 deletions apps/client/app/components/like-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export function LikeButton({ content, children }: Props) {
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.CONTENT_LIKES, 'user', currentUser.id],
})
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.COLLECTIONS],
})
}
}

Expand Down
1 change: 0 additions & 1 deletion apps/client/app/modules/tags/all/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,3 @@ export function TagsModule() {
</>
)
}

12 changes: 7 additions & 5 deletions apps/client/app/providers/react-query/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { QueryClient, QueryClientProvider, HydrationBoundary } from '@tanstack/react-query'
import {
QueryClient,
QueryClientProvider,
HydrationBoundary,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { type PropsWithChildren } from 'react'
import { useDehydratedState } from '@/hooks/use-dehydrated-state.ts'
Expand All @@ -15,13 +19,11 @@ const queryClient = new QueryClient({
})

export const ReactQueryProvider = ({ children }: PropsWithChildren) => {
const dehydratedState = useDehydratedState();
const dehydratedState = useDehydratedState()

return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydratedState}>
{children}
</HydrationBoundary>
<HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
Expand Down
2 changes: 1 addition & 1 deletion apps/client/app/routes/account_.upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export async function action({ request }: ActionFunctionArgs) {
})

// Let them see their contents.
return redirect('/account')
return redirect('/account/uploads')
} catch (error: unknown) {
if (error instanceof Error) {
return { error: error.message }
Expand Down
95 changes: 83 additions & 12 deletions services/main/Configurations/CacheProvider.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,102 @@
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
using StackExchange.Redis;

namespace main.Configuratons;

public class CacheProvider
{
private readonly IDistributedCache _cache;
private readonly IDatabase _redis;
private readonly IServer _redisServer;
public static readonly bool CacheEnabled = true; // Controls if caching is enabled or not appwide.
public static readonly string CachePrefix = "mfoni-";
public static readonly TimeSpan? CacheTTL = null;

public CacheProvider(IDistributedCache cache)
public static readonly Dictionary<string, string> CacheEntities = new Dictionary<string, string>
{
_cache = cache;
// Only caching content related entities for now.
{ "collections", "collections" },
{ "contents", "contents" },
{ "tags", "tags" },

// { "admins", "admins" },
// { "auth", "users" },
// { "waitlists", "waitlist" },
// { "users", "users" },
// { "creator-subscriptions", "subscriptions" },
// // TODO: update endpoint to get wallet for admins and make sure to update admins app.
// { "wallet-transactions", "wallet-transactions" },
// { "wallets", "wallets" },
// { "wallet", "wallets" },
};

public CacheProvider(IConnectionMultiplexer redis)
{
_redis = redis.GetDatabase();
_redisServer = redis.GetServer(redis.GetEndPoints()[0]);
}

public async Task<T?> GetFromCache<T>(string key)
{
var redisValue = await _redis.StringGetAsync($"{CachePrefix}{key}");
return redisValue.HasValue ? JsonSerializer.Deserialize<T>(redisValue!) : default(T);
}

public async Task SetCache<T>(string key, T value, TimeSpan? timeSpan)
{
var redisValue = JsonSerializer.Serialize(value);

if (typeof(T) == typeof(string))
{
redisValue = value as string;
}

// TODO: lock the cache key when making updates - integrate redlock.
await _redis.StringSetAsync($"{CachePrefix}{key}", redisValue, timeSpan);
}

public async Task ClearCache(RedisKey key)
{
await _redis.KeyDeleteAsync(key);
}

public async Task<T> GetFromCache<T>(string key)
public async Task UpdateCacheExpiry(RedisKey key, TimeSpan timeSpan)
{
var cachedUsers = await _cache.GetStringAsync(key);
return cachedUsers == null ? default(T) : JsonSerializer.Deserialize<T>(cachedUsers);
await _redis.KeyExpireAsync($"{CachePrefix}{key}", timeSpan);
}

public async Task SetCache<T>(string key, T value, DistributedCacheEntryOptions options)
public async Task EntityChanged(string[] Queries)
{
var users = JsonSerializer.Serialize(value);
await _cache.SetStringAsync(key, users, options);
if (CacheEnabled)
{
foreach (var query in Queries)
{
var result = _redisServer.Keys(pattern: $"{CachePrefix}*{query}");
await _redis.KeyDeleteAsync(result.ToArray());
}
}
}

public async Task ClearCache(string key)
public async Task<T> ResolveCache<T>(string key, Func<Task<T>> resolver)
{
await _cache.RemoveAsync(key);
if (CacheEnabled)
{
var cached = await GetFromCache<T>(key);
if (cached != null)
{

if (CacheTTL != null)
{
await UpdateCacheExpiry(key, (TimeSpan)CacheTTL);
}

return cached;
}

var result = await resolver();
await SetCache(key, result, CacheTTL);
return result;
}

return await resolver();
}
}
5 changes: 1 addition & 4 deletions services/main/Controllers/Admin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@ CacheProvider cacheProvider
public async Task<ActionResult> SaveCache()
{

await _cacheProvider.SetCache("test", new { test = "test" }, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});
await _cacheProvider.SetCache("test", new { test = "test" }, TimeSpan.FromMinutes(5));
return new ObjectResult(
new GetEntityResponse<bool>(
true,
Expand Down
2 changes: 1 addition & 1 deletion services/main/DTOs/OutputTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class OutputTag
public string? CreatedByAdminId { get; set; }
public OutputAdmin? CreatedByAdmin { get; set; }
public string? CreatedByUserId { get; set; }
public OutputUser? CreatedByUser { get; set; }
public OutputBasicUser? CreatedByUser { get; set; }
public required DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public required DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
Loading

0 comments on commit 43c7496

Please sign in to comment.