Last active
March 15, 2024 18:06
-
-
Save DLiblik/c8129f45a591a616fc4bc350a01d5393 to your computer and use it in GitHub Desktop.
Drop-in extension of Svelte's writable store that can be paused and resumed, where resuming the store causes only the latest update to the store to be published to subscribers (or none at all if no updates were made while paused).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { get, writable, type Updater, type Writable, type Subscriber, | |
type Unsubscriber, type Invalidator } from "svelte/store"; | |
/** A handle returned from a call to {@link PausableWritable.pause} */ | |
export type PauseHandle = {}; | |
/** | |
A {@link Writable} that supports pausing and resuming store subscription | |
updates, enabling throttling of high-volume subscription notifications if | |
a large number of individual changes are to occur together and intermiediary | |
state is not valuable. | |
A handle is returned after each call to {@link pause} which must be provided | |
to {@link resume} to resume the pause. | |
Multiple to calls to pause are tracked; the store will remain paused until | |
all outstanding handles are resumed. | |
Once resumed, if any updates to the store occurred during the pause, only | |
the final value will be updated to subscribers (not all the in-between | |
changes, if any). | |
{@link pauseDuringOperation} and {@link pauseDuringOperationAsync} convenience | |
functions are provided to allow the store to be paused while a provided | |
function is run. | |
*/ | |
export interface PausableWritable<T> extends Writable<T> { | |
/** | |
Pauses the store from notifying subscribers of updates until the store | |
is resumed again. | |
A handle is returned which must be provided to {@link resume} to resume | |
the pause. | |
Multiple to calls to pause are tracked; the store will remain paused until | |
all outstanding handles are resumed. | |
When paused, the returned store will suppress updates to subscribers, but | |
will continue to track changes. | |
Any new subscriptions (or calls to {@link get} a store's value, which | |
internally does this by using a temporary subscription) will get the | |
value of the store _prior_ to the store being paused. | |
*/ | |
pause():PauseHandle; | |
/** | |
Resumes the updating of subscribers to the store after a prior call | |
to {@link pause}. | |
Once resumed, if any updates to the store occurred during the pause, only | |
the final value will be updated to subscribers (not all the in-between | |
changes, if any). | |
@param handle Handle obtained from calling {@link pause} to resume. | |
*/ | |
resume(handle:PauseHandle):void; | |
/** | |
Pauses the store while running a provided function, then resumes the store | |
once the function is complete (or errors out). | |
If the provided operation errors, the store will be resumed prior to the | |
error throwing out of this method. | |
@param opFunc Operation to perform while the store is paused. | |
*/ | |
pauseDuringOperation<T>(opFunc:() => T):T; | |
/** | |
Pauses the store while running a provided async function, then resumes the | |
store once the function is complete (or errors out). | |
If the provided operation errors, the store will be resumed prior to the | |
error throwing out of this method. | |
@param opFunc Operation to perform while the store is paused. | |
*/ | |
pauseDuringOperationAsync<T>(opFunc:() => Promise<T>):Promise<T>; | |
} | |
class PausableWritableClass<T> implements PausableWritable<T> { | |
private state:Writable<T>; | |
private pauseHandles:any[] = []; | |
get isPaused() { | |
return this.pauseHandles.length > 0; | |
} | |
private hasPending = false; | |
private toSet?:T; | |
constructor(value:T|undefined) { | |
this.state = writable(value); | |
} | |
set(value: T):void { | |
if (this.isPaused) { | |
this.toSet = value; | |
this.hasPending = true; | |
return; | |
} | |
this.state.set(value); | |
} | |
update(updater: Updater<T>):void { | |
if (this.isPaused) { | |
const curVal = this.hasPending ? (this.toSet as T) : get(this.state); | |
this.toSet = updater(curVal); | |
this.hasPending = true; | |
return; | |
} | |
this.state.update(updater); | |
} | |
subscribe(run: Subscriber<T>, invalidate?: Invalidator<T> | undefined): Unsubscriber { | |
return this.state.subscribe((val) => { | |
return run(val); | |
}, invalidate); | |
} | |
pause() { | |
const handle = {}; | |
this.pauseHandles.push(handle); | |
return handle; | |
} | |
resume(handle:any) { | |
if (!this.pauseHandles.includes(handle)) { | |
return; | |
} | |
this.pauseHandles = this.pauseHandles.filter(h => h !== handle); | |
if (!this.pauseHandles.length && this.hasPending) { | |
const toSet = this.toSet as T; | |
this.hasPending = false; | |
this.toSet = undefined; | |
this.state.set(toSet); | |
} | |
} | |
pauseDuringOperation<T>(opFunc:() => T):T { | |
const ph = this.pause(); | |
try { | |
return opFunc(); | |
} | |
finally { | |
this.resume(ph); | |
} | |
} | |
async pauseDuringOperationAsync<T>(opFunc:() => Promise<T>):Promise<T> { | |
const ph = this.pause(); | |
try { | |
return await opFunc(); | |
} | |
finally { | |
this.resume(ph); | |
} | |
} | |
} | |
/** | |
Creates a Svelte {@link Writable} that can also be paused and resumed. | |
Calling {@link PausableWritable.pause} returns a handle that is required to | |
then resume the store. | |
Multiple calls to pause are tracked; the store will remain paused until all | |
outstanding handles are resumed. | |
When paused, the returned store will suppress updates to subscribers, but | |
will continue to track changes. | |
Any new subscriptions (or calls to {@link get} a store's value, which | |
internally does this by using a temporary subscription) will get the value | |
of the store _prior_ to the store being paused. | |
Once resumed, if any updates to the store occurred during the pause, only | |
the final value will be updated to subscribers (not all the in-between | |
changes, if any). | |
@param initVal Initial value to set into the store. | |
@returns A {@link Writable<T>} that can be paused and resumed. | |
*/ | |
export function pausableWritable<T>(initVal?:T):PausableWritable<T> { | |
return new PausableWritableClass(initVal); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment