Skip to content

Commit 1d366e2

Browse files
committed
TEST IMPLEMENATION OF CROSS-CLIENTLOADER-REQUEST DEDUPE
1 parent 7488263 commit 1d366e2

File tree

5 files changed

+197
-28
lines changed

5 files changed

+197
-28
lines changed

apps/cyberstorm-remix/app/c/community.tsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,10 @@ import { faDiscord } from "@fortawesome/free-brands-svg-icons";
2323
import { faArrowUpRight } from "@fortawesome/pro-solid-svg-icons";
2424
import { DapperTs } from "@thunderstore/dapper-ts";
2525
import { type OutletContextShape } from "../root";
26-
import {
27-
getPublicEnvVariables,
28-
getSessionTools,
29-
} from "cyberstorm/security/publicEnvVariables";
26+
import { getPublicEnvVariables } from "cyberstorm/security/publicEnvVariables";
3027
import { Suspense } from "react";
3128
import { classnames } from "@thunderstore/cyberstorm/src/utils/utils";
29+
import { getRequestScopedDapper } from "cyberstorm/utils/dapperSingleton";
3230

3331
export async function loader({ params }: LoaderFunctionArgs) {
3432
if (params.communityId) {
@@ -47,15 +45,9 @@ export async function loader({ params }: LoaderFunctionArgs) {
4745
throw new Response("Community not found", { status: 404 });
4846
}
4947

50-
export async function clientLoader({ params }: LoaderFunctionArgs) {
48+
export async function clientLoader({ params, request }: LoaderFunctionArgs) {
5149
if (params.communityId) {
52-
const tools = getSessionTools();
53-
const dapper = new DapperTs(() => {
54-
return {
55-
apiHost: tools?.getConfig().apiHost,
56-
sessionId: tools?.getConfig().sessionId,
57-
};
58-
});
50+
const dapper = getRequestScopedDapper(request);
5951
const community = dapper.getCommunity(params.communityId);
6052
return {
6153
community: community,

apps/cyberstorm-remix/app/entry.client.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import { hydrateRoot } from "react-dom/client";
44
import { useLocation, useMatches } from "react-router";
55
import { HydratedRouter } from "react-router/dom";
66

7-
import { getPublicEnvVariables } from "cyberstorm/security/publicEnvVariables";
7+
import {
8+
getPublicEnvVariables,
9+
getSessionTools,
10+
} from "cyberstorm/security/publicEnvVariables";
811
import { denyUrls } from "cyberstorm/utils/sentry";
12+
import { primeClientDapper } from "cyberstorm/utils/dapperSingleton";
913

1014
const publicEnvVariables = getPublicEnvVariables([
1115
"VITE_SITE_URL",
@@ -15,6 +19,11 @@ const publicEnvVariables = getPublicEnvVariables([
1519
"VITE_CLIENT_SENTRY_DSN",
1620
]);
1721

22+
const sessionTools = getSessionTools();
23+
primeClientDapper(() =>
24+
sessionTools.getConfig(publicEnvVariables.VITE_API_URL)
25+
);
26+
1827
Sentry.init({
1928
dsn: publicEnvVariables.VITE_CLIENT_SENTRY_DSN,
2029
integrations: [

apps/cyberstorm-remix/app/p/tabs/Readme/Readme.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import { Await, type LoaderFunctionArgs } from "react-router";
22
import { useLoaderData } from "react-router";
33
import { DapperTs } from "@thunderstore/dapper-ts";
4-
import {
5-
getPublicEnvVariables,
6-
getSessionTools,
7-
} from "cyberstorm/security/publicEnvVariables";
4+
import { getPublicEnvVariables } from "cyberstorm/security/publicEnvVariables";
85
import { Suspense } from "react";
96
import { SkeletonBox } from "@thunderstore/cyberstorm";
107
import "./Readme.css";
8+
import { getRequestScopedDapper } from "cyberstorm/utils/dapperSingleton";
119

1210
export async function loader({ params }: LoaderFunctionArgs) {
13-
if (params.namespaceId && params.packageId) {
11+
if (params.communityId && params.namespaceId && params.packageId) {
1412
const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]);
1513
const dapper = new DapperTs(() => {
1614
return {
1715
apiHost: publicEnvVariables.VITE_API_URL,
1816
sessionId: undefined,
1917
};
2018
});
19+
const community = await dapper.getCommunity(params.communityId);
2120
return {
2221
readme: dapper.getPackageReadme(params.namespaceId, params.packageId),
22+
community: community,
2323
};
2424
}
2525
return {
@@ -29,17 +29,13 @@ export async function loader({ params }: LoaderFunctionArgs) {
2929
};
3030
}
3131

32-
export async function clientLoader({ params }: LoaderFunctionArgs) {
33-
if (params.namespaceId && params.packageId) {
34-
const tools = getSessionTools();
35-
const dapper = new DapperTs(() => {
36-
return {
37-
apiHost: tools?.getConfig().apiHost,
38-
sessionId: tools?.getConfig().sessionId,
39-
};
40-
});
32+
export async function clientLoader({ params, request }: LoaderFunctionArgs) {
33+
if (params.communityId && params.namespaceId && params.packageId) {
34+
const dapper = getRequestScopedDapper(request);
35+
const community = dapper.getCommunity(params.communityId);
4136
return {
4237
readme: dapper.getPackageReadme(params.namespaceId, params.packageId),
38+
community,
4339
};
4440
}
4541
return {
@@ -50,9 +46,10 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
5046
}
5147

5248
export default function Readme() {
53-
const { status, message, readme } = useLoaderData<
49+
const { status, message, readme, community } = useLoaderData<
5450
typeof loader | typeof clientLoader
5551
>();
52+
console.log(community);
5653

5754
if (status === "error") return <div>{message}</div>;
5855
return (
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import isEqual from "lodash/isEqual";
2+
3+
const restrictedNames = new Set(["", "anonymous", "default"]);
4+
5+
type CacheEntry = {
6+
funcName: string;
7+
inputs: unknown[];
8+
promise: Promise<unknown>;
9+
};
10+
11+
const requestScopedCaches = new WeakMap<Request, CacheEntry[]>();
12+
const globalFallbackCache: CacheEntry[] = [];
13+
14+
function getCache(request?: Request) {
15+
if (!request) {
16+
return globalFallbackCache;
17+
}
18+
19+
let cache = requestScopedCaches.get(request);
20+
if (!cache) {
21+
cache = [];
22+
requestScopedCaches.set(request, cache);
23+
24+
const signal = request.signal;
25+
if (signal && !signal.aborted) {
26+
signal.addEventListener(
27+
"abort",
28+
() => {
29+
cache?.splice(0, cache.length);
30+
},
31+
{ once: true }
32+
);
33+
}
34+
}
35+
36+
return cache;
37+
}
38+
39+
export function getCachedDapperPromise<Args extends unknown[], Result>(
40+
method: (...inputs: Args) => Promise<Result>,
41+
inputs: Args,
42+
request?: Request,
43+
label?: string
44+
): Promise<Result> {
45+
const methodLabel = label ?? method.name;
46+
47+
if (restrictedNames.has(methodLabel)) {
48+
throw new Error(
49+
"Dapper methods must be named functions to support caching."
50+
);
51+
}
52+
53+
const cache = getCache(request);
54+
for (const cacheEntry of cache) {
55+
if (
56+
cacheEntry.funcName === methodLabel &&
57+
isEqual(cacheEntry.inputs, inputs)
58+
) {
59+
return cacheEntry.promise as Promise<Result>;
60+
}
61+
}
62+
63+
const promise = method(...inputs) as Promise<Result>;
64+
cache.push({
65+
funcName: methodLabel,
66+
inputs,
67+
promise,
68+
});
69+
70+
return promise;
71+
}
72+
73+
export function clearGlobalDapperCache() {
74+
globalFallbackCache.splice(0, globalFallbackCache.length);
75+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { DapperTs } from "@thunderstore/dapper-ts";
2+
import { type RequestConfig } from "@thunderstore/thunderstore-api";
3+
import { getSessionTools } from "cyberstorm/security/publicEnvVariables";
4+
import { getCachedDapperPromise } from "./dapperRequestCache";
5+
6+
type ConfigFactory = () => RequestConfig;
7+
8+
let currentConfigFactory: ConfigFactory | undefined;
9+
const requestScopedProxies = new WeakMap<Request, DapperTs>();
10+
11+
function resolveConfigFactory(): ConfigFactory {
12+
if (currentConfigFactory) {
13+
return currentConfigFactory;
14+
}
15+
16+
const tools = getSessionTools();
17+
currentConfigFactory = () => tools.getConfig();
18+
return currentConfigFactory;
19+
}
20+
21+
function updateDapperConfig(factory: ConfigFactory) {
22+
currentConfigFactory = factory;
23+
if (typeof window !== "undefined" && window.Dapper) {
24+
window.Dapper.config = () => factory();
25+
}
26+
}
27+
28+
export function primeClientDapper(factory?: ConfigFactory) {
29+
if (typeof window === "undefined") {
30+
return;
31+
}
32+
33+
if (factory) {
34+
updateDapperConfig(factory);
35+
} else if (!currentConfigFactory) {
36+
resolveConfigFactory();
37+
}
38+
39+
if (!window.Dapper) {
40+
const resolvedFactory = resolveConfigFactory();
41+
window.Dapper = new DapperTs(() => resolvedFactory());
42+
}
43+
}
44+
45+
export function getClientDapper(): DapperTs {
46+
if (typeof window === "undefined") {
47+
throw new Error("getClientDapper can only run in the browser");
48+
}
49+
50+
if (!window.Dapper) {
51+
primeClientDapper();
52+
}
53+
54+
return window.Dapper;
55+
}
56+
57+
export function getRequestScopedDapper(request?: Request): DapperTs {
58+
if (!request) {
59+
return getClientDapper();
60+
}
61+
62+
let proxy = requestScopedProxies.get(request);
63+
if (proxy) {
64+
return proxy;
65+
}
66+
67+
const baseDapper = getClientDapper();
68+
const handler: ProxyHandler<DapperTs> = {
69+
get(target, prop, receiver) {
70+
const value = Reflect.get(target, prop, receiver);
71+
if (typeof value !== "function") {
72+
return value;
73+
}
74+
75+
if (typeof prop !== "string" || !prop.startsWith("get")) {
76+
return value.bind(target);
77+
}
78+
79+
return (...args: unknown[]) =>
80+
getCachedDapperPromise(
81+
(...innerArgs: unknown[]) =>
82+
(value as (...i: unknown[]) => Promise<unknown>).apply(
83+
target,
84+
innerArgs
85+
),
86+
args as unknown[],
87+
request,
88+
prop
89+
);
90+
},
91+
};
92+
93+
proxy = new Proxy(baseDapper, handler) as DapperTs;
94+
requestScopedProxies.set(request, proxy);
95+
return proxy;
96+
}

0 commit comments

Comments
 (0)