Skip to content

Commit 3000ab1

Browse files
committed
working
1 parent fd70c44 commit 3000ab1

File tree

2 files changed

+330
-173
lines changed

2 files changed

+330
-173
lines changed

src/async-stores/index-copy.ts

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import { get, type Updater, type Readable, writable } from 'svelte/store';
2+
import type {
3+
AsyncStoreOptions,
4+
Loadable,
5+
LoadState,
6+
State,
7+
Stores,
8+
StoresValues,
9+
WritableLoadable,
10+
VisitedMap,
11+
} from './types.js';
12+
import {
13+
anyReloadable,
14+
getStoresArray,
15+
reloadAll,
16+
loadAll,
17+
} from '../utils/index.js';
18+
import { flagStoreCreated, getStoreTestingMode, logError } from '../config.js';
19+
20+
// STORES
21+
22+
const getLoadState = (stateString: State): LoadState => {
23+
return {
24+
isLoading: stateString === 'LOADING',
25+
isReloading: stateString === 'RELOADING',
26+
isLoaded: stateString === 'LOADED',
27+
isWriting: stateString === 'WRITING',
28+
isError: stateString === 'ERROR',
29+
isPending: stateString === 'LOADING' || stateString === 'RELOADING',
30+
isSettled: stateString === 'LOADED' || stateString === 'ERROR',
31+
};
32+
};
33+
34+
/**
35+
* Generate a Loadable store that is considered 'loaded' after resolving synchronous or asynchronous behavior.
36+
* This behavior may be derived from the value of parent Loadable or non Loadable stores.
37+
* If so, this store will begin loading only after the parents have loaded.
38+
* This store is also writable. It includes a `set` function that will immediately update the value of the store
39+
* and then execute provided asynchronous behavior to persist this change.
40+
* @param stores Any readable or array of Readables whose value is used to generate the asynchronous behavior of this store.
41+
* Any changes to the value of these stores post-load will restart the asynchronous behavior of the store using the new values.
42+
* @param mappingLoadFunction A function that takes in the values of the stores and generates a Promise that resolves
43+
* to the final value of the store when the asynchronous behavior is complete.
44+
* @param mappingWriteFunction A function that takes in the new value of the store and uses it to perform async behavior.
45+
* Typically this would be to persist the change. If this value resolves to a value the store will be set to it.
46+
* @param options Modifiers for store behavior.
47+
* @returns A Loadable store whose value is set to the resolution of provided async behavior.
48+
* The loaded value of the store will be ready after awaiting the load function of this store.
49+
*/
50+
export const asyncWritable = <S extends Stores, T>(
51+
stores: S,
52+
mappingLoadFunction: (values: StoresValues<S>) => Promise<T> | T,
53+
mappingWriteFunction?: (
54+
value: T,
55+
parentValues?: StoresValues<S>,
56+
oldValue?: T
57+
) => Promise<void | T>,
58+
options: AsyncStoreOptions<T> = {}
59+
): WritableLoadable<T> => {
60+
flagStoreCreated();
61+
const { reloadable, trackState, initial } = options;
62+
63+
const loadState = trackState
64+
? writable<LoadState>(getLoadState('LOADING'))
65+
: undefined;
66+
67+
const setState = (state: State) => loadState?.set(getLoadState(state));
68+
69+
// stringified representation of parents' loaded values
70+
// used to track whether a change has occurred and the store reloaded
71+
let loadedValuesString: string;
72+
73+
let latestLoadAndSet: () => Promise<T>;
74+
75+
// most recent call of mappingLoadFunction, including resulting side effects
76+
// (updating store value, tracking state, etc)
77+
let currentLoadPromise: Promise<T>;
78+
79+
const tryLoad = async (values: StoresValues<S>) => {
80+
try {
81+
return await mappingLoadFunction(values);
82+
} catch (e) {
83+
if (e.name !== 'AbortError') {
84+
logError(e);
85+
setState('ERROR');
86+
}
87+
throw e;
88+
}
89+
};
90+
91+
// eslint-disable-next-line prefer-const
92+
let loadDependenciesThenSet: (
93+
parentLoadFunction: (stores: S) => Promise<StoresValues<S>>,
94+
forceReload?: boolean
95+
) => Promise<T>;
96+
97+
const thisStore = writable(initial, () => {
98+
loadDependenciesThenSet(loadAll).catch(() => Promise.resolve());
99+
100+
const parentUnsubscribers = getStoresArray(stores).map((store) =>
101+
store.subscribe(() => {
102+
loadDependenciesThenSet(loadAll).catch(() => Promise.resolve());
103+
})
104+
);
105+
106+
return () => {
107+
parentUnsubscribers.map((unsubscriber) => unsubscriber());
108+
};
109+
});
110+
111+
loadDependenciesThenSet = async (
112+
parentLoadFunction: (stores: S) => Promise<StoresValues<S>>,
113+
forceReload = false
114+
) => {
115+
const loadParentStores = parentLoadFunction(stores);
116+
117+
try {
118+
await loadParentStores;
119+
} catch {
120+
currentLoadPromise = loadParentStores as Promise<T>;
121+
setState('ERROR');
122+
return currentLoadPromise;
123+
}
124+
125+
const storeValues = getStoresArray(stores).map((store) =>
126+
get(store)
127+
) as StoresValues<S>;
128+
129+
if (!forceReload) {
130+
const newValuesString = JSON.stringify(storeValues);
131+
if (newValuesString === loadedValuesString) {
132+
// no change, don't generate new promise
133+
return currentLoadPromise;
134+
}
135+
loadedValuesString = newValuesString;
136+
}
137+
138+
// convert storeValues to single store value if expected by mapping function
139+
const loadInput = Array.isArray(stores) ? storeValues : storeValues[0];
140+
141+
const loadAndSet = async () => {
142+
latestLoadAndSet = loadAndSet;
143+
if (get(loadState)?.isSettled) {
144+
setState('RELOADING');
145+
}
146+
try {
147+
const finalValue = await tryLoad(loadInput);
148+
thisStore.set(finalValue);
149+
setState('LOADED');
150+
return finalValue;
151+
} catch (e) {
152+
// if a load is aborted, resolve to the current value of the store
153+
if (e.name === 'AbortError') {
154+
// Normally when a load is aborted we want to leave the state as is.
155+
// However if the latest load is aborted we change back to LOADED
156+
// so that it does not get stuck LOADING/RELOADIN'.
157+
if (loadAndSet === latestLoadAndSet) {
158+
setState('LOADED');
159+
}
160+
return get(thisStore);
161+
}
162+
throw e;
163+
}
164+
};
165+
166+
currentLoadPromise = loadAndSet();
167+
return currentLoadPromise;
168+
};
169+
170+
const setStoreValueThenWrite = async (
171+
updater: Updater<T>,
172+
persist?: boolean
173+
) => {
174+
setState('WRITING');
175+
let oldValue: T;
176+
try {
177+
oldValue = await loadDependenciesThenSet(loadAll);
178+
} catch {
179+
oldValue = get(thisStore);
180+
}
181+
const newValue = updater(oldValue);
182+
currentLoadPromise = currentLoadPromise
183+
.then(() => newValue)
184+
.catch(() => newValue);
185+
thisStore.set(newValue);
186+
187+
if (mappingWriteFunction && persist) {
188+
try {
189+
const parentValues = await loadAll(stores);
190+
191+
const writeResponse = (await mappingWriteFunction(
192+
newValue,
193+
parentValues,
194+
oldValue
195+
)) as T;
196+
197+
if (writeResponse !== undefined) {
198+
thisStore.set(writeResponse);
199+
currentLoadPromise = currentLoadPromise.then(() => writeResponse);
200+
}
201+
} catch (e) {
202+
logError(e);
203+
setState('ERROR');
204+
throw e;
205+
}
206+
}
207+
setState('LOADED');
208+
};
209+
210+
// required properties
211+
const subscribe = thisStore.subscribe;
212+
const set = (newValue: T, persist = true) =>
213+
setStoreValueThenWrite(() => newValue, persist);
214+
const update = (updater: Updater<T>, persist = true) =>
215+
setStoreValueThenWrite(updater, persist);
216+
const load = () => loadDependenciesThenSet(loadAll);
217+
218+
// // optional properties
219+
const hasReloadFunction = Boolean(reloadable || anyReloadable(stores));
220+
const reload = hasReloadFunction
221+
? async (visitedMap?: VisitedMap) => {
222+
const visitMap = visitedMap ?? new WeakMap();
223+
const reloadAndTrackVisits = (stores: S) => reloadAll(stores, visitMap);
224+
setState('RELOADING');
225+
const result = await loadDependenciesThenSet(
226+
reloadAndTrackVisits,
227+
reloadable
228+
);
229+
setState('LOADED');
230+
return result;
231+
}
232+
: undefined;
233+
234+
const state: Readable<LoadState> = loadState
235+
? { subscribe: loadState.subscribe }
236+
: undefined;
237+
const reset = getStoreTestingMode()
238+
? () => {
239+
thisStore.set(initial);
240+
setState('LOADING');
241+
loadedValuesString = undefined;
242+
currentLoadPromise = undefined;
243+
}
244+
: undefined;
245+
246+
return {
247+
get store() {
248+
return this;
249+
},
250+
subscribe,
251+
set,
252+
update,
253+
load,
254+
...(reload && { reload }),
255+
...(state && { state }),
256+
...(reset && { reset }),
257+
};
258+
};
259+
260+
/**
261+
* Generate a Loadable store that is considered 'loaded' after resolving asynchronous behavior.
262+
* This asynchronous behavior may be derived from the value of parent Loadable or non Loadable stores.
263+
* If so, this store will begin loading only after the parents have loaded.
264+
* @param stores Any readable or array of Readables whose value is used to generate the asynchronous behavior of this store.
265+
* Any changes to the value of these stores post-load will restart the asynchronous behavior of the store using the new values.
266+
* @param mappingLoadFunction A function that takes in the values of the stores and generates a Promise that resolves
267+
* to the final value of the store when the asynchronous behavior is complete.
268+
* @param options Modifiers for store behavior.
269+
* @returns A Loadable store whose value is set to the resolution of provided async behavior.
270+
* The loaded value of the store will be ready after awaiting the load function of this store.
271+
*/
272+
export const asyncDerived = <S extends Stores, T>(
273+
stores: S,
274+
mappingLoadFunction: (values: StoresValues<S>) => Promise<T>,
275+
options?: AsyncStoreOptions<T>
276+
): Loadable<T> => {
277+
const { store, subscribe, load, reload, state, reset } = asyncWritable(
278+
stores,
279+
mappingLoadFunction,
280+
undefined,
281+
options
282+
);
283+
284+
return {
285+
store,
286+
subscribe,
287+
load,
288+
...(reload && { reload }),
289+
...(state && { state }),
290+
...(reset && { reset }),
291+
};
292+
};
293+
294+
/**
295+
* Generates a Loadable store that will start asynchronous behavior when subscribed to,
296+
* and whose value will be equal to the resolution of that behavior when completed.
297+
* @param initial The initial value of the store before it has loaded or upon load failure.
298+
* @param loadFunction A function that generates a Promise that resolves to the final value
299+
* of the store when the asynchronous behavior is complete.
300+
* @param options Modifiers for store behavior.
301+
* @returns A Loadable store whose value is set to the resolution of provided async behavior.
302+
* The loaded value of the store will be ready after awaiting the load function of this store.
303+
*/
304+
export const asyncReadable = <T>(
305+
initial: T,
306+
loadFunction: () => Promise<T>,
307+
options?: Omit<AsyncStoreOptions<T>, 'initial'>
308+
): Loadable<T> => {
309+
return asyncDerived([], loadFunction, { ...options, initial });
310+
};

0 commit comments

Comments
 (0)