nuqs-svelte
Version:
Svelte adaptation of the `nuqs` library for managing URL query strings as state.
148 lines (147 loc) • 5.47 kB
JavaScript
import { debug } from "./debug";
import { error } from "./errors";
import { getDefaultThrottle } from "./utils";
export const FLUSH_RATE_LIMIT_MS = getDefaultThrottle();
const updateQueue = new Map();
const queueOptions = {
history: "replace",
scroll: false,
shallow: true,
throttleMs: FLUSH_RATE_LIMIT_MS,
};
let lastFlushTimestamp = 0;
let flushPromiseCache = null;
export function getQueuedValue(key) {
return updateQueue.get(key);
}
export function resetQueue() {
updateQueue.clear();
queueOptions.history = "replace";
queueOptions.scroll = false;
queueOptions.shallow = true;
queueOptions.throttleMs = FLUSH_RATE_LIMIT_MS;
}
export function enqueueQueryStringUpdate(key, value, serialize, options) {
const serializedOrNull = value === null ? null : serialize(value);
debug("[nuqs queue] Enqueueing %s=%s %O", key, serializedOrNull, options);
updateQueue.set(key, serializedOrNull);
// Any item can override an option for the whole batch of updates
if (options.history === "push") {
queueOptions.history = "push";
}
if (options.scroll) {
queueOptions.scroll = true;
}
if (options.shallow === false) {
queueOptions.shallow = false;
}
queueOptions.throttleMs = Math.max(options.throttleMs ?? FLUSH_RATE_LIMIT_MS, Number.isFinite(queueOptions.throttleMs) ? queueOptions.throttleMs : 0);
return serializedOrNull;
}
function getSearchParamsSnapshotFromLocation() {
return new URLSearchParams(location.search);
}
/**
* Eventually flush the update queue to the URL query string.
*
* This takes care of throttling to avoid hitting browsers limits
* on calls to the history pushState/replaceState APIs, and defers
* the call so that individual query state updates can be batched
* when running in the same event loop tick.
*
* @returns a Promise to the URLSearchParams that have been applied.
*/
export function scheduleFlushToURL({ getSearchParamsSnapshot = getSearchParamsSnapshotFromLocation, updateUrl, rateLimitFactor = 1, }) {
if (flushPromiseCache === null) {
flushPromiseCache = new Promise((resolve, reject) => {
if (!Number.isFinite(queueOptions.throttleMs)) {
debug("[nuqs queue] Skipping flush due to throttleMs=Infinity");
resolve(getSearchParamsSnapshot());
// Let the promise be returned before clearing the cached value
setTimeout(() => {
flushPromiseCache = null;
}, 0);
return;
}
function flushNow() {
lastFlushTimestamp = performance.now();
const [search, error] = flushUpdateQueue({
updateUrl,
getSearchParamsSnapshot,
});
if (error === null) {
resolve(search);
}
else {
reject(search);
}
flushPromiseCache = null;
}
// We run the logic on the next event loop tick to allow
// multiple query updates to set their own throttleMs value.
function runOnNextTick() {
const now = performance.now();
const timeSinceLastFlush = now - lastFlushTimestamp;
const throttleMs = queueOptions.throttleMs;
const flushInMs = rateLimitFactor * Math.max(0, Math.min(throttleMs, throttleMs - timeSinceLastFlush));
debug("[nuqs queue] Scheduling flush in %f ms. Throttled at %f ms", flushInMs, throttleMs);
if (flushInMs === 0) {
// Since we're already in the "next tick" from queued updates,
// no need to do setTimeout(0) here.
flushNow();
}
else {
setTimeout(flushNow, flushInMs);
}
}
setTimeout(runOnNextTick, 0);
});
}
return flushPromiseCache;
}
function flushUpdateQueue({ updateUrl, getSearchParamsSnapshot, }) {
const search = getSearchParamsSnapshot();
if (updateQueue.size === 0) {
return [search, null];
}
// Work on a copy and clear the queue immediately
const items = Array.from(updateQueue.entries());
const options = { ...queueOptions };
// Restore defaults
resetQueue();
debug("[nuqs queue] Flushing queue %O with options %O", items, options);
for (const [key, value] of items) {
if (value === null) {
search.delete(key);
}
else {
search.set(key, value);
}
}
try {
compose(() => {
updateUrl(search, {
history: options.history,
scroll: options.scroll,
shallow: options.shallow,
throttleMs: options.throttleMs,
});
});
return [search, null];
}
catch (err) {
// This may fail due to rate-limiting of history methods,
// for example Safari only allows 100 updates in a 30s window.
console.error(error(429), items.map(([key]) => key).join(), err);
return [search, err];
}
}
export function compose(final) {
const recursiveCompose = (index) => {
if (index === 0) {
return final();
}
return recursiveCompose(index + 1);
};
recursiveCompose(0);
}