Skip to content

Instantly share code, notes, and snippets.

@DLiblik
Last active March 15, 2024 18:06
Show Gist options
  • Save DLiblik/c8129f45a591a616fc4bc350a01d5393 to your computer and use it in GitHub Desktop.
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).
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