55 writable ,
66 StartStopNotifier ,
77 readable ,
8+ Writable ,
89} from 'svelte/store' ;
910import type {
1011 AsyncStoreOptions ,
@@ -15,6 +16,7 @@ import type {
1516 StoresValues ,
1617 WritableLoadable ,
1718 VisitedMap ,
19+ AsyncLoadable ,
1820} from './types.js' ;
1921import {
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 */
5961export 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 */
274342export 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 */
306375export 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