Skip to content

Commit

Permalink
Remove fetch caching (#280)
Browse files Browse the repository at this point in the history
* Improve anySignal() parameters

* Remove local fetch caching

* Remove mentions of cache in the readme
  • Loading branch information
kmcginnes committed Apr 4, 2024
1 parent 8691898 commit 2e9d1fd
Show file tree
Hide file tree
Showing 10 changed files with 45 additions and 174 deletions.
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,6 @@ If either of the Graph Explorer or the proxy-server are served over an HTTPS con
Note: To get rid of the “Not Secure” warning, see [Using self-signed certificates on Chrome](./additionaldocs/development.md#using-self-signed-certificates-on-chrome).
### Connection Cache
Setting up a new connection (or editing an existing connection) allows you to enable a cache for the connector requests. The cache store is configured to use the browser IndexedDB that allows you to make use of data stored between sessions. The time that the data stored in the cache is also configurable, by default it has a lifetime of 10 minutes.
The purpose of the cache is to avoid making multiple requests to the database with the same criteria. Therefore, a request with particular parameters will be cached at most the time set just with the response obtained. After that time, if the exact same request is made again, the response will be updated and stored again.
## Authentication
Authentication for Amazon Neptune connections is enabled using the [SigV4 signing protocol](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html).
Expand Down
5 changes: 2 additions & 3 deletions packages/graph-explorer/src/connector/gremlin/useGremlin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,13 @@ const useGremlin = () => {

const fetchSchemaFunc = useCallback(
async options => {
const ops = { ...options, disableCache: true };
let summary;
try {
const response = await useFetch.request(
`${url}/pg/statistics/summary?mode=detailed`,
{
method: "GET",
...ops,
...options,
}
);
summary = (response.payload.graphSummary as GraphSummary) || undefined;
Expand All @@ -62,7 +61,7 @@ const useGremlin = () => {
console.error("[Summary API]", e);
}
}
return fetchSchema(_gremlinFetch(ops), summary);
return fetchSchema(_gremlinFetch(options), summary);
},
[_gremlinFetch, url, useFetch]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const useOpenCypher = () => {
"Content-Type": "application/json",
},
body: JSON.stringify({ query: queryTemplate }),
disableCache: options?.disableCache,
...options,
});
};
Expand All @@ -36,8 +35,6 @@ const useOpenCypher = () => {

const fetchSchemaFunc = useCallback(
async (options: any) => {
const ops = { ...options, disableCache: true };

let summary;
try {
const endpoint =
Expand All @@ -46,7 +43,7 @@ const useOpenCypher = () => {
: `${url}/summary?mode=detailed`;
const response = await useFetch.request(endpoint, {
method: "GET",
...ops,
...options,
});

summary =
Expand All @@ -58,7 +55,7 @@ const useOpenCypher = () => {
console.error("[Summary API]", e);
}
}
return fetchSchema(_openCypherFetch(ops), summary);
return fetchSchema(_openCypherFetch(options), summary);
},
[_openCypherFetch, url, useFetch, serviceType]
);
Expand Down
5 changes: 2 additions & 3 deletions packages/graph-explorer/src/connector/sparql/useSPARQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,13 @@ const useSPARQL = (blankNodes: BlankNodesMap) => {

const fetchSchemaFunc = useCallback(
async options => {
const ops = { ...options, disableCache: true };
let summary;
try {
const response = await useFetch.request(
`${url}/rdf/statistics/summary?mode=detailed`,
{
method: "GET",
...ops,
...options,
}
);
summary = (response.payload.graphSummary as GraphSummary) || undefined;
Expand All @@ -176,7 +175,7 @@ const useSPARQL = (blankNodes: BlankNodesMap) => {
console.error("[Summary API]", e);
}
}
return fetchSchema(_sparqlFetch(ops), summary);
return fetchSchema(_sparqlFetch(options), summary);
},
[_sparqlFetch, url, useFetch]
);
Expand Down
110 changes: 26 additions & 84 deletions packages/graph-explorer/src/connector/useGEFetch.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import { useCallback } from "react";
import localforage from "localforage";
import { CacheItem } from "./useGEFetchTypes";
import { useConfiguration, type ConnectionConfig } from "../core";
import { DEFAULT_SERVICE_TYPE } from "../utils/constants";
import { anySignal } from "./utils/anySignal";

// 10 minutes
const CACHE_TIME_MS = 10 * 60 * 1000;

const localforageCache = localforage.createInstance({
name: "ge",
version: 1.0,
storeName: "connector-cache",
});

/**
* Attempts to decode the error response into a JSON object.
*
Expand Down Expand Up @@ -58,49 +47,8 @@ const useGEFetch = () => {
const connection = useConfiguration()?.connection as
| ConnectionConfig
| undefined;
const _getFromCache = useCallback(
async key => {
if (!connection?.enableCache) {
return;
}

return localforageCache.getItem(key) as Promise<CacheItem | undefined>;
},
[connection?.enableCache]
);

const _setToCache = useCallback(
async (key, value) => {
if (connection?.enableCache) {
return;
}

return localforageCache.setItem(key, value);
},
[connection?.enableCache]
);

const _requestAndCache = useCallback(
async (
url: URL,
options: (RequestInit & { disableCache: boolean }) | undefined
) => {
const response = await fetch(url, options);
if (!response.ok) {
const error = await decodeErrorSafely(response);
throw new Error("Network response was not OK", { cause: error });
}

// A successful response is assumed to be JSON
const data = await response.json();
if (options?.disableCache !== true) {
_setToCache(url, { data, updatedAt: new Date().getTime() });
}
return data as any;
},
[_setToCache]
);

// Construct the request headers based on the connection settings
const getAuthHeaders = useCallback(
typeHeaders => {
const headers: HeadersInit = {};
Expand All @@ -124,46 +72,40 @@ const useGEFetch = () => {
]
);

const request = useCallback(
async (uri, options) => {
const cachedResponse = await _getFromCache(uri);
if (
cachedResponse &&
cachedResponse.updatedAt + (connection?.cacheTimeMs ?? CACHE_TIME_MS) >
new Date().getTime()
) {
return cachedResponse.data;
}
// Construct an AbortSignal for the fetch timeout if configured
const getFetchTimeoutSignal = useCallback(() => {
if (!connection?.fetchTimeoutMs) {
return null;
}

if (connection.fetchTimeoutMs <= 0) {
return null;
}

return AbortSignal.timeout(connection.fetchTimeoutMs);
}, [connection?.fetchTimeoutMs]);

const request = useCallback(
async (uri: URL | RequestInfo, options: RequestInit) => {
// Apply connection settings to fetch options
const fetchOptions: RequestInit = {
...options,
headers: getAuthHeaders(options.headers),
signal: anySignal(getFetchTimeoutSignal(), options.signal),
};

const connectionFetchTimeout = connection?.fetchTimeoutMs;
const response = await fetch(uri, fetchOptions);

if (connectionFetchTimeout && connectionFetchTimeout > 0) {
const timeoutSignal = AbortSignal.timeout(connectionFetchTimeout);

// Combine timeout with existing signal
if (options.signal) {
fetchOptions.signal = anySignal([timeoutSignal, options.signal]);
} else {
fetchOptions.signal = timeoutSignal;
}
if (!response.ok) {
const error = await decodeErrorSafely(response);
throw new Error("Network response was not OK", { cause: error });
}

return _requestAndCache(uri, {
...options,
...fetchOptions,
}) as Promise<any>;
// A successful response is assumed to be JSON
const data = await response.json();
return data;
},
[
_getFromCache,
_requestAndCache,
connection?.cacheTimeMs,
connection?.fetchTimeoutMs,
getAuthHeaders,
]
[getAuthHeaders, getFetchTimeoutSignal]
);

return {
Expand Down
7 changes: 0 additions & 7 deletions packages/graph-explorer/src/connector/useGEFetchTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import {
} from "../core";

export type QueryOptions = RequestInit & {
disableCache?: boolean;
queryId?: string;
successCallback?: (queryId: string) => void;
};

export type VertexSchemaResponse = Pick<
Expand Down Expand Up @@ -190,8 +188,3 @@ export type ConfigurationWithConnection = Omit<
"connection"
> &
Required<Pick<ConfigurationContextProps, "connection">>;

export type CacheItem = {
updatedAt: number;
data: any;
};
18 changes: 13 additions & 5 deletions packages/graph-explorer/src/connector/utils/anySignal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@
*
* Requires at least node.js 18.
*
* @param signals An array of AbortSignal values that will be merged.
* @returns A single AbortSignal value.
* @param signals A variable amount of AbortSignal values that will be merged in to one.
* @returns A single AbortSignal value or undefined if none are passed.
*/
export function anySignal(signals: AbortSignal[]): AbortSignal {
const controller = new AbortController();
export function anySignal(
...signals: (AbortSignal | null | undefined)[]
): AbortSignal | undefined {
// Filter out null or undefined signals
const filteredSignals = signals.flatMap(s => (s ? [s] : []));

if (filteredSignals.length === 0) {
return undefined;
}

for (const signal of signals) {
const controller = new AbortController();
for (const signal of filteredSignals) {
if (signal.aborted) {
// Exiting early if one of the signals
// is already aborted.
Expand Down
10 changes: 0 additions & 10 deletions packages/graph-explorer/src/core/ConfigurationProvider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,6 @@ export type ConnectionConfig = {
* It is needed to sign requests.
*/
awsRegion?: string;
/**
* Enable or disable connector cache.
* By default, it's enabled.
*/
enableCache?: boolean;
/**
* Number of milliseconds before expiring a cached request.
* By default, 10 minutes.
*/
cacheTimeMs?: number;
/**
* Number of milliseconds before aborting a request.
* By default, 60 seconds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ const ConnectorProvider = ({ children }: PropsWithChildren<any>) => {
"graphDbUrl",
"awsAuthEnabled",
"awsRegion",
"enableCache",
"cacheTimeMs",
"fetchTimeoutMs",
] as const,
[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ type ConnectionForm = {
awsAuthEnabled?: boolean;
serviceType?: "neptune-db" | "neptune-graph";
awsRegion?: string;
enableCache?: boolean;
cacheTimeMs?: number;
fetchTimeMs?: number;
};

Expand Down Expand Up @@ -78,8 +76,6 @@ const CreateConnection = ({
awsAuthEnabled: data.awsAuthEnabled,
serviceType: data.serviceType,
awsRegion: data.awsRegion,
enableCache: data.enableCache,
cacheTimeMs: data.cacheTimeMs * 60 * 1000,
fetchTimeoutMs: data.fetchTimeMs,
},
};
Expand Down Expand Up @@ -108,7 +104,6 @@ const CreateConnection = ({
awsAuthEnabled: data.awsAuthEnabled,
serviceType: data.serviceType,
awsRegion: data.awsRegion,
cacheTimeMs: data.cacheTimeMs * 60 * 1000,
fetchTimeoutMs: data.fetchTimeMs,
},
});
Expand Down Expand Up @@ -150,8 +145,6 @@ const CreateConnection = ({
awsAuthEnabled: initialData?.awsAuthEnabled || false,
serviceType: initialData?.serviceType || "neptune-db",
awsRegion: initialData?.awsRegion || "",
enableCache: true,
cacheTimeMs: (initialData?.cacheTimeMs ?? 10 * 60 * 1000) / 60000,
fetchTimeMs: initialData?.fetchTimeMs,
});

Expand Down Expand Up @@ -316,48 +309,6 @@ const CreateConnection = ({
</>
)}
</div>
<div className={pfx("configuration-form")}>
<Checkbox
value={"enableCache"}
checked={form.enableCache}
onChange={e => {
onFormChange("enableCache")(e.target.checked);
}}
styles={{
label: {
display: "block",
},
}}
label={
<div style={{ display: "flex", alignItems: "center", gap: 2 }}>
Enable Cache
<Tooltip
text={
<div style={{ maxWidth: 300 }}>
Requests made by the Graph Explorer can be temporarily
stored in the browser cache for quick access to the data.
</div>
}
>
<div>
<InfoIcon style={{ width: 18, height: 18 }} />
</div>
</Tooltip>
</div>
}
/>
{form.enableCache && (
<div className={pfx("input-url")}>
<Input
label="Cache Time (minutes)"
type={"number"}
value={form.cacheTimeMs}
onChange={onFormChange("cacheTimeMs")}
min={0}
/>
</div>
)}
</div>
<div className={pfx("configuration-form")}>
<Checkbox
value={"fetchTimeoutMs"}
Expand Down

0 comments on commit 2e9d1fd

Please sign in to comment.