Skip to content

Commit ac37827

Browse files
authored
Use promise based local forage Recoil effect (#883)
* Mock localForage with a reasonable stub * Use asyncLocalForageEffect * Remove hacks to make localforage synchronous * Update changelog
1 parent 2f7d2ef commit ac37827

File tree

10 files changed

+78
-142
lines changed

10 files changed

+78
-142
lines changed

Changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
last sync timestamp (thanks @enumura1,
99
[#901](https://github.com/aws/graph-explorer/pull/901),
1010
[#908](https://github.com/aws/graph-explorer/pull/908))
11+
- **Updated** localForage Recoil integration to be async and use Suspense
12+
([#883](https://github.com/aws/graph-explorer/pull/883))
1113

1214
## Release v1.15.0
1315

packages/graph-explorer/src/core/AppStatusLoader.tsx

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
1-
import { PropsWithChildren, startTransition, useEffect } from "react";
1+
import { PropsWithChildren, startTransition, Suspense, useEffect } from "react";
22
import { useLocation } from "react-router";
33
import { useRecoilState, useRecoilValue } from "recoil";
44
import { LoadingSpinner, PanelEmptyState } from "@/components";
55
import Redirect from "@/components/Redirect";
66
import {
77
activeConfigurationAtom,
88
configurationAtom,
9-
isStoreLoadedAtom,
109
} from "./StateProvider/configuration";
1110
import { schemaAtom } from "./StateProvider/schema";
12-
import useLoadStore from "./StateProvider/useLoadStore";
1311
import { logger } from "@/utils";
1412
import { useQuery } from "@tanstack/react-query";
1513
import { fetchDefaultConnection } from "./defaultConnection";
1614

17-
const AppStatusLoader = ({ children }: PropsWithChildren) => {
15+
function AppStatusLoader({ children }: PropsWithChildren) {
16+
return (
17+
<Suspense fallback={<PreparingEnvironment />}>
18+
<LoadDefaultConfig>{children}</LoadDefaultConfig>
19+
</Suspense>
20+
);
21+
}
22+
23+
function LoadDefaultConfig({ children }: PropsWithChildren) {
1824
const location = useLocation();
19-
useLoadStore();
20-
const isStoreLoaded = useRecoilValue(isStoreLoadedAtom);
25+
2126
const [activeConfig, setActiveConfig] = useRecoilState(
2227
activeConfigurationAtom
2328
);
@@ -29,17 +34,12 @@ const AppStatusLoader = ({ children }: PropsWithChildren) => {
2934
queryFn: fetchDefaultConnection,
3035
staleTime: Infinity,
3136
// Run the query only if the store is loaded and there are no configs
32-
enabled: isStoreLoaded && configuration.size === 0,
37+
enabled: configuration.size === 0,
3338
});
3439

3540
const defaultConnectionConfigs = defaultConfigQuery.data;
3641

3742
useEffect(() => {
38-
if (!isStoreLoaded) {
39-
logger.debug("Store not loaded, skipping default connection load");
40-
return;
41-
}
42-
4343
if (configuration.size > 0) {
4444
logger.debug(
4545
"Connections already exist, skipping default connection load"
@@ -66,23 +66,11 @@ const AppStatusLoader = ({ children }: PropsWithChildren) => {
6666
}, [
6767
activeConfig,
6868
configuration,
69-
isStoreLoaded,
7069
setActiveConfig,
7170
setConfiguration,
7271
defaultConnectionConfigs,
7372
]);
7473

75-
// Wait until state is recovered from the indexed DB
76-
if (!isStoreLoaded) {
77-
return (
78-
<PanelEmptyState
79-
title="Preparing environment..."
80-
subtitle="We are loading all components"
81-
icon={<LoadingSpinner />}
82-
/>
83-
);
84-
}
85-
8674
if (configuration.size === 0 && defaultConfigQuery.isLoading) {
8775
return (
8876
<PanelEmptyState
@@ -120,6 +108,16 @@ const AppStatusLoader = ({ children }: PropsWithChildren) => {
120108
}
121109

122110
return <>{children}</>;
123-
};
111+
}
112+
113+
function PreparingEnvironment() {
114+
return (
115+
<PanelEmptyState
116+
title="Preparing environment..."
117+
subtitle="We are loading all components"
118+
icon={<LoadingSpinner />}
119+
/>
120+
);
121+
}
124122

125123
export default AppStatusLoader;

packages/graph-explorer/src/core/StateProvider/configuration.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
RawConfiguration,
1010
VertexTypeConfig,
1111
} from "@/core";
12-
import localForageEffect from "./localForageEffect";
12+
import { localForageEffect } from "./localForageEffect";
1313
import { activeSchemaSelector, SchemaInference } from "./schema";
1414
import {
1515
EdgePreferences,
@@ -23,21 +23,16 @@ import {
2323
RESERVED_TYPES_PROPERTY,
2424
} from "@/utils/constants";
2525

26-
export const isStoreLoadedAtom = atom<boolean>({
27-
key: "store-loaded",
28-
default: false,
29-
});
30-
3126
export const activeConfigurationAtom = atom<ConfigurationId | null>({
3227
key: "active-configuration",
3328
default: null,
34-
effects: [localForageEffect()],
29+
effects: [localForageEffect("active-configuration")],
3530
});
3631

3732
export const configurationAtom = atom<Map<ConfigurationId, RawConfiguration>>({
3833
key: "configuration",
3934
default: new Map(),
40-
effects: [localForageEffect()],
35+
effects: [localForageEffect("configuration")],
4136
});
4237

4338
/** Gets or sets the config that is currently active. */

packages/graph-explorer/src/core/StateProvider/graphSession/storage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { atom, DefaultValue, selector } from "recoil";
22
import { EdgeId, VertexId } from "../../entities";
33
import { activeConfigurationAtom } from "../configuration";
44
import { ConfigurationId } from "../../ConfigurationProvider";
5-
import localForageEffect from "../localForageEffect";
5+
import { localForageEffect } from "../localForageEffect";
66

77
/** A model for the graph data that is stored in local storage. */
88
export type GraphSessionStorageModel = {
@@ -19,7 +19,7 @@ export const isRestorePreviousSessionAvailableAtom = atom({
1919
export const allGraphSessionsAtom = atom({
2020
key: "graph-sessions",
2121
default: new Map<ConfigurationId, GraphSessionStorageModel>(),
22-
effects: [localForageEffect()],
22+
effects: [localForageEffect("graph-sessions")],
2323
});
2424

2525
/** Gets or sets the active connection's graph session data. */
Lines changed: 15 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,12 @@
11
import localForage from "localforage";
22
import { AtomEffect, DefaultValue } from "recoil";
3-
import { logger } from "@/utils";
43

54
localForage.config({
65
name: "ge",
76
version: 1.0,
87
storeName: "graph-explorer",
98
});
109

11-
// The first time that the atom is loaded from the store,
12-
// mark as loaded to avoid side effect on asynchronous events
13-
// that can load the atom state before it is recovered from the store
14-
export const loadedAtoms: Set<string> = new Set();
15-
16-
const localForageEffect =
17-
<T>(): AtomEffect<T> =>
18-
({ setSelf, onSet, trigger, node }) => {
19-
// If there's a persisted value - set it on load
20-
const loadPersisted = async () => {
21-
const savedValue = await localForage.getItem(node.key);
22-
23-
if (savedValue != null) {
24-
setSelf(savedValue as T | DefaultValue);
25-
return;
26-
}
27-
};
28-
29-
if (trigger === "get") {
30-
loadPersisted().then(() => {
31-
loadedAtoms.add(node.key);
32-
});
33-
}
34-
35-
// Subscribe to state changes and persist them to localForage
36-
onSet((newValue: T, _: T | DefaultValue, isReset: boolean) => {
37-
isReset
38-
? localForage.removeItem(node.key)
39-
: localForage.setItem(node.key, newValue);
40-
});
41-
};
42-
43-
// Reference docs:
44-
// https://recoiljs.org/docs/guides/atom-effects#asynchronous-storage
45-
4610
/**
4711
* Loads and sets data asynchronously to localForage. Must be used within a
4812
* Suspense and ErrorBoundary.
@@ -51,31 +15,27 @@ const localForageEffect =
5115
* @returns An AtomEffect that will connect Recoil to localForage
5216
* asynchronously.
5317
*/
54-
export function asyncLocalForageEffect<T>(key: string): AtomEffect<T> {
55-
logger.debug(`[${key}] Async local forage effect created`);
56-
57-
return ({ setSelf, onSet }) => {
58-
setSelf(
59-
localForage.getItem(key).then(
60-
savedValue =>
61-
savedValue != null
62-
? (savedValue as T | DefaultValue)
63-
: new DefaultValue() // Abort initialization if no value was stored
64-
)
65-
);
18+
export function localForageEffect<T>(key: string): AtomEffect<T> {
19+
return ({ setSelf, onSet, trigger }) => {
20+
// Load saved value
21+
if (trigger === "get") {
22+
setSelf(
23+
localForage.getItem<T>(key).then(savedValue => {
24+
return savedValue != null ? savedValue : new DefaultValue();
25+
})
26+
);
27+
}
6628

6729
// Subscribe to state changes and persist them to localForage
68-
onSet((newValue: T, _, isReset) => {
30+
onSet((newValue, _, isReset) => {
6931
if (isReset) {
70-
logger.debug(`[${key}] Resetting value`, newValue);
7132
localForage.removeItem(key);
72-
return;
33+
} else {
34+
localForage.setItem(key, newValue);
7335
}
74-
75-
logger.debug(`[${key}] Setting value`, newValue);
76-
localForage.setItem(key, newValue);
7736
});
7837
};
7938
}
8039

81-
export default localForageEffect;
40+
// Reference docs:
41+
// https://recoiljs.org/docs/guides/atom-effects#asynchronous-storage

packages/graph-explorer/src/core/StateProvider/schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
PrefixTypeConfig,
66
VertexTypeConfig,
77
} from "@/core/ConfigurationProvider";
8-
import localForageEffect from "./localForageEffect";
8+
import { localForageEffect } from "./localForageEffect";
99
import { activeConfigurationAtom } from "./configuration";
1010
import { Edge, Entities, toEdgeMap, toNodeMap, Vertex } from "@/core";
1111
import { logger, sanitizeText } from "@/utils";
@@ -26,7 +26,7 @@ export type SchemaInference = {
2626
export const schemaAtom = atom<Map<string, SchemaInference>>({
2727
key: "schema",
2828
default: new Map(),
29-
effects: [localForageEffect()],
29+
effects: [localForageEffect("schema")],
3030
});
3131

3232
export const activeSchemaSelector = selector({

packages/graph-explorer/src/core/StateProvider/useLoadStore.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

packages/graph-explorer/src/core/StateProvider/userPreferences.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { atom, DefaultValue, selectorFamily, useSetRecoilState } from "recoil";
2-
import localForageEffect from "./localForageEffect";
2+
import { localForageEffect } from "./localForageEffect";
33
import { useCallback } from "react";
44

55
export type ShapeStyle =
@@ -120,7 +120,7 @@ export type SidebarItems = UserPreferences["layout"]["activeSidebarItem"];
120120
export const userStylingAtom = atom<UserStyling>({
121121
key: "user-styling",
122122
default: {},
123-
effects: [localForageEffect()],
123+
effects: [localForageEffect("user-styling")],
124124
});
125125

126126
export const userStylingNodeAtom = selectorFamily({
@@ -211,7 +211,7 @@ export const userLayoutAtom = atom<UserPreferences["layout"]>({
211211
height: 300,
212212
},
213213
},
214-
effects: [localForageEffect()],
214+
effects: [localForageEffect("user-layout")],
215215
});
216216

217217
export function useCloseSidebar() {

packages/graph-explorer/src/core/featureFlags.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import { atom, selector, useRecoilValue } from "recoil";
2-
import { asyncLocalForageEffect } from "./StateProvider/localForageEffect";
2+
import { localForageEffect } from "./StateProvider/localForageEffect";
33

44
/** Shows Recoil diff logs in the browser console. */
55
export const showRecoilStateLoggingAtom = atom({
66
key: "feature-flag-recoil-state-logging",
77
default: false,
8-
effects: [asyncLocalForageEffect("showRecoilStateLogging")],
8+
effects: [localForageEffect("showRecoilStateLogging")],
99
});
1010

1111
/** Shows debug actions in various places around the app. */
1212
export const showDebugActionsAtom = atom({
1313
key: "feature-flag-debug-actions",
1414
default: false,
15-
effects: [asyncLocalForageEffect("showDebugActions")],
15+
effects: [localForageEffect("showDebugActions")],
1616
});
1717

1818
/** Shows debug actions in various places around the app. */
1919
export const allowLoggingDbQueryAtom = atom({
2020
key: "feature-flag-db-query-logging",
2121
default: false,
22-
effects: [asyncLocalForageEffect("allowLoggingDbQuery")],
22+
effects: [localForageEffect("allowLoggingDbQuery")],
2323
});
2424

2525
export type FeatureFlags = {

0 commit comments

Comments
 (0)