Skip to content

Commit b2be866

Browse files
committed
broke something
1 parent 7cc4482 commit b2be866

File tree

9 files changed

+442
-269
lines changed

9 files changed

+442
-269
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ notes:
55
- state store now always included
66
- all mapping load functions are rebounced by default
77
- if an async store loses all subscriptions and then gains one the mapping load function will always evaluate even if the inputs have not changed
8+
- can't use stores to hold errors
89

910
## 1.0.17 (2023-6-20)
1011

src/async-stores/index.ts

Lines changed: 122 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
writable,
66
StartStopNotifier,
77
readable,
8+
Writable,
89
} from 'svelte/store';
910
import type {
1011
AsyncStoreOptions,
@@ -15,6 +16,7 @@ import type {
1516
StoresValues,
1617
WritableLoadable,
1718
VisitedMap,
19+
AsyncLoadable,
1820
} from './types.js';
1921
import {
2022
anyReloadable,
@@ -48,30 +50,33 @@ const getLoadState = (stateString: State): LoadState => {
4850
* and then execute provided asynchronous behavior to persist this change.
4951
* @param stores Any readable or array of Readables whose value is used to generate the asynchronous behavior of this store.
5052
* Any changes to the value of these stores post-load will restart the asynchronous behavior of the store using the new values.
51-
* @param mappingLoadFunction A function that takes in the values of the stores and generates a Promise that resolves
53+
* @param selfLoadFunction A function that takes in the loaded values of any parent stores and generates a Promise that resolves
5254
* to the final value of the store when the asynchronous behavior is complete.
53-
* @param mappingWriteFunction A function that takes in the new value of the store and uses it to perform async behavior.
55+
* @param writePersistFunction A function that takes in the new value of the store and uses it to perform async behavior.
5456
* Typically this would be to persist the change. If this value resolves to a value the store will be set to it.
5557
* @param options Modifiers for store behavior.
5658
* @returns A Loadable store whose value is set to the resolution of provided async behavior.
5759
* The loaded value of the store will be ready after awaiting the load function of this store.
5860
*/
5961
export const asyncWritable = <S extends Stores, T>(
6062
stores: S,
61-
mappingLoadFunction: (values: StoresValues<S>) => Promise<T> | T,
62-
mappingWriteFunction?: (
63+
selfLoadFunction: (values: StoresValues<S>) => Promise<T> | T,
64+
writePersistFunction?: (
6365
value: T,
6466
parentValues?: StoresValues<S>,
6567
oldValue?: T
6668
) => Promise<void | T>,
6769
options: AsyncStoreOptions<T> = {}
6870
): WritableLoadable<T> => {
71+
// eslint-disable-next-line prefer-const
72+
let thisStore: Writable<T>;
73+
6974
flagStoreCreated();
70-
const { reloadable, initial, debug } = options;
75+
const { reloadable, initial, debug, rebounceDelay } = options;
7176

72-
const debuggy = debug ? console.log : undefined;
77+
const debuggy = debug ? (...args) => console.log(debug, ...args) : undefined;
7378

74-
const rebouncedMappingLoad = rebounce(mappingLoadFunction);
79+
const rebouncedSelfLoad = rebounce(selfLoadFunction, rebounceDelay);
7580

7681
const loadState = writable<LoadState>(getLoadState('LOADING'));
7782
const setState = (state: State) => loadState.set(getLoadState(state));
@@ -82,47 +87,81 @@ export const asyncWritable = <S extends Stores, T>(
8287

8388
// most recent call of mappingLoadFunction, including resulting side effects
8489
// (updating store value, tracking state, etc)
85-
let currentLoadPromise: Promise<T>;
86-
let resolveCurrentLoad: (value: T | PromiseLike<T>) => void;
87-
let rejectCurrentLoad: (reason: Error) => void;
90+
let currentLoadPromise: Promise<T | Error>;
91+
let resolveCurrentLoad: (value: T | PromiseLike<T> | Error) => void;
8892

8993
const setCurrentLoadPromise = () => {
90-
currentLoadPromise = new Promise((resolve, reject) => {
94+
debuggy?.('setCurrentLoadPromise -> new load promise generated');
95+
currentLoadPromise = new Promise((resolve) => {
9196
resolveCurrentLoad = resolve;
92-
rejectCurrentLoad = reject;
9397
});
9498
};
9599

100+
const getLoadedValueOrThrow = async (callback?: () => void) => {
101+
debuggy?.('getLoadedValue -> starting await current load');
102+
const result = await currentLoadPromise;
103+
debuggy?.('getLoadedValue -> got loaded result', result);
104+
callback?.();
105+
if (result instanceof Error) {
106+
throw result;
107+
}
108+
return currentLoadPromise as T;
109+
};
110+
96111
let parentValues: StoresValues<S>;
97112

98-
const mappingLoadThenSet = async (setStoreValue) => {
113+
let mostRecentLoadTracker: Record<string, never>;
114+
const selfLoadThenSet = async () => {
99115
if (get(loadState).isSettled) {
100116
setCurrentLoadPromise();
101117
debuggy?.('setting RELOADING');
102118
setState('RELOADING');
103119
}
104120

121+
const thisLoadTracker = {};
122+
mostRecentLoadTracker = thisLoadTracker;
123+
105124
try {
106-
const finalValue = await rebouncedMappingLoad(parentValues);
125+
// parentValues
126+
const finalValue = (await rebouncedSelfLoad(parentValues)) as T;
107127
debuggy?.('setting value');
108-
setStoreValue(finalValue);
128+
thisStore.set(finalValue);
129+
109130
if (!get(loadState).isWriting) {
110131
debuggy?.('setting LOADED');
111132
setState('LOADED');
112133
}
113134
resolveCurrentLoad(finalValue);
114-
} catch (e) {
115-
if (e.name !== 'AbortError') {
116-
logError(e);
135+
} catch (error) {
136+
debuggy?.('caught error', error);
137+
if (error.name !== 'AbortError') {
138+
logError(error);
117139
setState('ERROR');
118-
rejectCurrentLoad(e);
140+
debuggy?.('resolving current load with error', error);
141+
// Resolve with an Error rather than rejecting so that unhandled rejections
142+
// are not created by the stores internal processes. These errors are
143+
// converted back to promise rejections via the load or reload functions,
144+
// allowing for proper handling after that point.
145+
// If your stack trace takes you here, make sure your store's
146+
// selfLoadFunction rejects with an Error to preserve the full trace.
147+
resolveCurrentLoad(error instanceof Error ? error : new Error(error));
148+
} else if (thisLoadTracker === mostRecentLoadTracker) {
149+
// Normally when a load is aborted we want to leave the state as is.
150+
// However if the latest load is aborted we change back to LOADED
151+
// so that it does not get stuck LOADING/RELOADING.
152+
setState('LOADED');
153+
resolveCurrentLoad(get(thisStore));
119154
}
120155
}
121156
};
122157

123-
const onFirstSubscription: StartStopNotifier<T> = (setStoreValue) => {
158+
let cleanupSubscriptions: () => void;
159+
160+
// called when store receives its first subscriber
161+
const onFirstSubscription: StartStopNotifier<T> = () => {
124162
setCurrentLoadPromise();
125163
parentValues = getAll(stores);
164+
setState('LOADING');
126165

127166
const initialLoad = async () => {
128167
debuggy?.('initial load called');
@@ -131,10 +170,11 @@ export const asyncWritable = <S extends Stores, T>(
131170
debuggy?.('setting ready');
132171
ready = true;
133172
changeReceived = false;
134-
mappingLoadThenSet(setStoreValue);
173+
selfLoadThenSet();
135174
} catch (error) {
136-
console.log('wtf is happening', error);
137-
rejectCurrentLoad(error);
175+
ready = true;
176+
changeReceived = false;
177+
resolveCurrentLoad(error);
138178
}
139179
};
140180
initialLoad();
@@ -150,19 +190,21 @@ export const asyncWritable = <S extends Stores, T>(
150190
}
151191
if (ready) {
152192
debuggy?.('proceeding because ready');
153-
mappingLoadThenSet(setStoreValue);
193+
selfLoadThenSet();
154194
}
155195
})
156196
);
157197

158198
// called on losing last subscriber
159-
return () => {
199+
cleanupSubscriptions = () => {
160200
parentUnsubscribers.map((unsubscriber) => unsubscriber());
161201
ready = false;
202+
changeReceived = false;
162203
};
204+
cleanupSubscriptions();
163205
};
164206

165-
const thisStore = writable(initial, onFirstSubscription);
207+
thisStore = writable(initial, onFirstSubscription);
166208

167209
const setStoreValueThenWrite = async (
168210
updater: Updater<T>,
@@ -171,7 +213,7 @@ export const asyncWritable = <S extends Stores, T>(
171213
setState('WRITING');
172214
let oldValue: T;
173215
try {
174-
oldValue = await currentLoadPromise;
216+
oldValue = await getLoadedValueOrThrow();
175217
} catch {
176218
oldValue = get(thisStore);
177219
}
@@ -180,9 +222,9 @@ export const asyncWritable = <S extends Stores, T>(
180222
let newValue = updater(oldValue);
181223
thisStore.set(newValue);
182224

183-
if (mappingWriteFunction && persist) {
225+
if (writePersistFunction && persist) {
184226
try {
185-
const writeResponse = (await mappingWriteFunction(
227+
const writeResponse = (await writePersistFunction(
186228
newValue,
187229
parentValues,
188230
oldValue
@@ -196,56 +238,80 @@ export const asyncWritable = <S extends Stores, T>(
196238
logError(error);
197239
debuggy?.('setting ERROR');
198240
setState('ERROR');
199-
rejectCurrentLoad(error);
241+
resolveCurrentLoad(newValue);
200242
throw error;
201243
}
202244
}
245+
203246
setState('LOADED');
204247
resolveCurrentLoad(newValue);
205248
};
206249

207250
// required properties
208251
const subscribe = thisStore.subscribe;
252+
209253
const load = () => {
210254
const dummyUnsubscribe = thisStore.subscribe(() => {
211255
/* no-op */
212256
});
213-
currentLoadPromise
214-
.catch(() => {
215-
/* no-op */
216-
})
217-
.finally(dummyUnsubscribe);
218-
return currentLoadPromise;
257+
return getLoadedValueOrThrow(dummyUnsubscribe);
219258
};
259+
220260
const reload = async (visitedMap?: VisitedMap) => {
261+
const dummyUnsubscribe = thisStore.subscribe(() => {
262+
/* no-op */
263+
});
221264
ready = false;
222265
changeReceived = false;
223-
setCurrentLoadPromise();
266+
if (get(loadState).isSettled) {
267+
debuggy?.('new load promise');
268+
setCurrentLoadPromise();
269+
}
224270
debuggy?.('setting RELOADING from reload');
271+
const wasErrored = get(loadState).isError;
225272
setState('RELOADING');
226273

227274
const visitMap = visitedMap ?? new WeakMap();
228275
try {
229-
await reloadAll(stores, visitMap);
276+
parentValues = await reloadAll(stores, visitMap);
277+
debuggy?.('parentValues', parentValues);
230278
ready = true;
231-
if (changeReceived || reloadable) {
232-
mappingLoadThenSet(thisStore.set);
279+
debuggy?.(changeReceived, reloadable, wasErrored);
280+
if (changeReceived || reloadable || wasErrored) {
281+
selfLoadThenSet();
233282
} else {
234283
resolveCurrentLoad(get(thisStore));
235284
setState('LOADED');
236285
}
237286
} catch (error) {
238287
debuggy?.('caught error during reload');
239288
setState('ERROR');
240-
rejectCurrentLoad(error);
289+
resolveCurrentLoad(error);
241290
}
242-
return currentLoadPromise;
291+
return getLoadedValueOrThrow(dummyUnsubscribe);
243292
};
293+
244294
const set = (newValue: T, persist = true) =>
245295
setStoreValueThenWrite(() => newValue, persist);
246296
const update = (updater: Updater<T>, persist = true) =>
247297
setStoreValueThenWrite(updater, persist);
248298

299+
const abort = () => {
300+
rebouncedSelfLoad.abort();
301+
};
302+
303+
const reset = getStoreTestingMode()
304+
? () => {
305+
cleanupSubscriptions();
306+
thisStore.set(initial);
307+
setState('LOADING');
308+
ready = false;
309+
changeReceived = false;
310+
currentLoadPromise = undefined;
311+
setCurrentLoadPromise();
312+
}
313+
: undefined;
314+
249315
return {
250316
get store() {
251317
return this;
@@ -255,7 +321,9 @@ export const asyncWritable = <S extends Stores, T>(
255321
reload,
256322
set,
257323
update,
324+
abort,
258325
state: { subscribe: loadState.subscribe },
326+
...(reset && { reset }),
259327
};
260328
};
261329

@@ -265,20 +333,20 @@ export const asyncWritable = <S extends Stores, T>(
265333
* If so, this store will begin loading only after the parents have loaded.
266334
* @param stores Any readable or array of Readables whose value is used to generate the asynchronous behavior of this store.
267335
* Any changes to the value of these stores post-load will restart the asynchronous behavior of the store using the new values.
268-
* @param mappingLoadFunction A function that takes in the values of the stores and generates a Promise that resolves
336+
* @param selfLoadFunction A function that takes in the values of the stores and generates a Promise that resolves
269337
* to the final value of the store when the asynchronous behavior is complete.
270338
* @param options Modifiers for store behavior.
271339
* @returns A Loadable store whose value is set to the resolution of provided async behavior.
272340
* The loaded value of the store will be ready after awaiting the load function of this store.
273341
*/
274342
export const asyncDerived = <S extends Stores, T>(
275343
stores: S,
276-
mappingLoadFunction: (values: StoresValues<S>) => Promise<T>,
344+
selfLoadFunction: (values: StoresValues<S>) => Promise<T>,
277345
options?: AsyncStoreOptions<T>
278-
): Loadable<T> => {
279-
const { store, subscribe, load, reload, state, reset } = asyncWritable(
346+
): AsyncLoadable<T> => {
347+
const { store, subscribe, load, reload, state, abort, reset } = asyncWritable(
280348
stores,
281-
mappingLoadFunction,
349+
selfLoadFunction,
282350
undefined,
283351
options
284352
);
@@ -287,8 +355,9 @@ export const asyncDerived = <S extends Stores, T>(
287355
store,
288356
subscribe,
289357
load,
290-
...(reload && { reload }),
291-
...(state && { state }),
358+
reload,
359+
state,
360+
abort,
292361
...(reset && { reset }),
293362
};
294363
};
@@ -297,16 +366,16 @@ export const asyncDerived = <S extends Stores, T>(
297366
* Generates a Loadable store that will start asynchronous behavior when subscribed to,
298367
* and whose value will be equal to the resolution of that behavior when completed.
299368
* @param initial The initial value of the store before it has loaded or upon load failure.
300-
* @param loadFunction A function that generates a Promise that resolves to the final value
369+
* @param selfLoadFunction A function that generates a Promise that resolves to the final value
301370
* of the store when the asynchronous behavior is complete.
302371
* @param options Modifiers for store behavior.
303372
* @returns A Loadable store whose value is set to the resolution of provided async behavior.
304373
* The loaded value of the store will be ready after awaiting the load function of this store.
305374
*/
306375
export const asyncReadable = <T>(
307376
initial: T,
308-
loadFunction: () => Promise<T>,
377+
selfLoadFunction: () => Promise<T>,
309378
options?: Omit<AsyncStoreOptions<T>, 'initial'>
310-
): Loadable<T> => {
311-
return asyncDerived([], loadFunction, { ...options, initial });
379+
): AsyncLoadable<T> => {
380+
return asyncDerived([], selfLoadFunction, { ...options, initial });
312381
};

0 commit comments

Comments
 (0)