nuqs
Version:
360 lines (350 loc) • 11.9 kB
JavaScript
import { c as debug, s as error } from "./context-C4spomkL.js";
import { useCallback, useRef, useSyncExternalStore } from "react";
//#region src/lib/queues/rate-limiting.ts
function getDefaultThrottle() {
if (typeof window === "undefined") return 50;
if (!Boolean(window.GestureEvent)) return 50;
try {
const match = navigator.userAgent?.match(/version\/([\d\.]+) safari/i);
return parseFloat(match[1]) >= 17 ? 120 : 320;
} catch {
return 320;
}
}
function throttle(timeMs) {
return {
method: "throttle",
timeMs
};
}
function debounce(timeMs) {
return {
method: "debounce",
timeMs
};
}
const defaultRateLimit = throttle(getDefaultThrottle());
//#endregion
//#region src/lib/search-params.ts
function isAbsentFromUrl(query) {
return query === null || Array.isArray(query) && query.length === 0;
}
function write(serialized, key, searchParams) {
if (typeof serialized === "string") searchParams.set(key, serialized);
else {
searchParams.delete(key);
for (const v of serialized) searchParams.append(key, v);
if (!searchParams.has(key)) searchParams.set(key, "");
}
return searchParams;
}
//#endregion
//#region src/lib/emitter.ts
function createEmitter() {
const all = /* @__PURE__ */ new Map();
return {
on(type, handler) {
const handlers = all.get(type) || [];
handlers.push(handler);
all.set(type, handlers);
return () => this.off(type, handler);
},
off(type, handler) {
const handlers = all.get(type);
if (handlers) all.set(type, handlers.filter((h) => h !== handler));
},
emit(type, event) {
all.get(type)?.forEach((handler) => handler(event));
}
};
}
//#endregion
//#region src/lib/timeout.ts
function timeout(callback, ms, signal) {
function onTick() {
callback();
signal.removeEventListener("abort", onAbort);
}
const id = setTimeout(onTick, ms);
function onAbort() {
clearTimeout(id);
signal.removeEventListener("abort", onAbort);
}
signal.addEventListener("abort", onAbort);
}
//#endregion
//#region src/lib/with-resolvers.ts
function withResolvers() {
const P = Promise;
if (Promise.hasOwnProperty("withResolvers")) return Promise.withResolvers();
let resolve = () => {};
let reject = () => {};
return {
promise: new P((res, rej) => {
resolve = res;
reject = rej;
}),
resolve,
reject
};
}
//#endregion
//#region src/lib/compose.ts
function compose(fns, final) {
let next = final;
for (let i = fns.length - 1; i >= 0; i--) {
const fn = fns[i];
if (!fn) continue;
const prev = next;
next = () => fn(prev);
}
next();
}
//#endregion
//#region src/lib/queues/throttle.ts
function getSearchParamsSnapshotFromLocation() {
return new URLSearchParams(location.search);
}
var ThrottledQueue = class {
updateMap = /* @__PURE__ */ new Map();
options = {
history: "replace",
scroll: false,
shallow: true
};
timeMs = defaultRateLimit.timeMs;
transitions = /* @__PURE__ */ new Set();
resolvers = null;
controller = null;
lastFlushedAt = 0;
resetQueueOnNextPush = false;
push({ key, query, options }, timeMs = defaultRateLimit.timeMs) {
if (this.resetQueueOnNextPush) {
this.reset();
this.resetQueueOnNextPush = false;
}
debug("[nuqs gtq] Enqueueing %s=%s %O", key, query, options);
this.updateMap.set(key, query);
if (options.history === "push") this.options.history = "push";
if (options.scroll) this.options.scroll = true;
if (options.shallow === false) this.options.shallow = false;
if (options.startTransition) this.transitions.add(options.startTransition);
if (!Number.isFinite(this.timeMs) || timeMs > this.timeMs) this.timeMs = timeMs;
}
getQueuedQuery(key) {
return this.updateMap.get(key);
}
getPendingPromise({ getSearchParamsSnapshot = getSearchParamsSnapshotFromLocation }) {
return this.resolvers?.promise ?? Promise.resolve(getSearchParamsSnapshot());
}
flush({ getSearchParamsSnapshot = getSearchParamsSnapshotFromLocation, rateLimitFactor = 1, ...adapter }, processUrlSearchParams) {
this.controller ??= new AbortController();
if (!Number.isFinite(this.timeMs)) {
debug("[nuqs gtq] Skipping flush due to throttleMs=Infinity");
return Promise.resolve(getSearchParamsSnapshot());
}
if (this.resolvers) return this.resolvers.promise;
this.resolvers = withResolvers();
const flushNow = () => {
this.lastFlushedAt = performance.now();
const [search, error] = this.applyPendingUpdates({
...adapter,
autoResetQueueOnUpdate: adapter.autoResetQueueOnUpdate ?? true,
getSearchParamsSnapshot
}, processUrlSearchParams);
if (error === null) {
this.resolvers.resolve(search);
this.resetQueueOnNextPush = true;
} else this.resolvers.reject(search);
this.resolvers = null;
};
const runOnNextTick = () => {
const timeSinceLastFlush = performance.now() - this.lastFlushedAt;
const timeMs = this.timeMs;
const flushInMs = rateLimitFactor * Math.max(0, timeMs - timeSinceLastFlush);
debug(`[nuqs gtq] Scheduling flush in %f ms. Throttled at %f ms (x%f)`, flushInMs, timeMs, rateLimitFactor);
if (flushInMs === 0) flushNow();
else timeout(flushNow, flushInMs, this.controller.signal);
};
timeout(runOnNextTick, 0, this.controller.signal);
return this.resolvers.promise;
}
abort() {
this.controller?.abort();
this.controller = new AbortController();
this.resolvers?.resolve(new URLSearchParams());
this.resolvers = null;
return this.reset();
}
reset() {
const queuedKeys = Array.from(this.updateMap.keys());
debug("[nuqs gtq] Resetting queue %s", JSON.stringify(Object.fromEntries(this.updateMap)));
this.updateMap.clear();
this.transitions.clear();
this.options = {
history: "replace",
scroll: false,
shallow: true
};
this.timeMs = defaultRateLimit.timeMs;
return queuedKeys;
}
applyPendingUpdates(adapter, processUrlSearchParams) {
const { updateUrl, getSearchParamsSnapshot } = adapter;
let search = getSearchParamsSnapshot();
debug(`[nuqs gtq] Applying %d pending update(s) on top of %s`, this.updateMap.size, search.toString());
if (this.updateMap.size === 0) return [search, null];
const items = Array.from(this.updateMap.entries());
const options = { ...this.options };
const transitions = Array.from(this.transitions);
if (adapter.autoResetQueueOnUpdate) this.reset();
debug("[nuqs gtq] Flushing queue %O with options %O", items, options);
for (const [key, value] of items) if (value === null) search.delete(key);
else search = write(value, key, search);
if (processUrlSearchParams) search = processUrlSearchParams(search);
try {
compose(transitions, () => {
updateUrl(search, options);
});
return [search, null];
} catch (err) {
console.error(error(429), items.map(([key]) => key).join(), err);
return [search, err];
}
}
};
const globalThrottleQueue = new ThrottledQueue();
//#endregion
//#region src/lib/queues/useSyncExternalStores.ts
/**
* Like `useSyncExternalStore`, but for subscribing to multiple keys.
*
* Each key becomes the key of the returned object,
* and the value is the result of calling `getKeySnapshot` with that key.
*
* @param keys - A list of keys to subscribe to.
* @param subscribeKey - A function that takes a key and a callback,
* subscribes to an external store using that key (calling the callback when
* state changes occur), and returns a function to unsubscribe from that key.
* @param getKeySnapshot - A function that takes a key and returns the snapshot for that key.
* It will be called on the server and on the client, so it needs to handle both
* environments.
*/
function useSyncExternalStores(keys, subscribeKey, getKeySnapshot) {
const snapshot = useCallback(() => {
const record = Object.fromEntries(keys.map((key) => [key, getKeySnapshot(key)]));
return [JSON.stringify(record), record];
}, [keys.join(","), getKeySnapshot]);
const cacheRef = useRef(null);
if (cacheRef.current === null) cacheRef.current = snapshot();
return useSyncExternalStore(useCallback((callback) => {
const off = keys.map((key) => subscribeKey(key, callback));
return () => off.forEach((unsubscribe) => unsubscribe());
}, [keys.join(","), subscribeKey]), () => {
const [cacheKey, record] = snapshot();
if (cacheRef.current[0] === cacheKey) return cacheRef.current[1];
cacheRef.current = [cacheKey, record];
return record;
}, () => cacheRef.current[1]);
}
//#endregion
//#region src/lib/queues/debounce.ts
var DebouncedPromiseQueue = class {
callback;
resolvers = withResolvers();
controller = new AbortController();
queuedValue = void 0;
constructor(callback) {
this.callback = callback;
}
abort() {
this.controller.abort();
this.queuedValue = void 0;
}
push(value, timeMs) {
this.queuedValue = value;
this.controller.abort();
this.controller = new AbortController();
timeout(() => {
const outputResolvers = this.resolvers;
try {
debug("[nuqs dq] Flushing debounce queue", value);
const callbackPromise = this.callback(value);
debug("[nuqs dq] Reset debounce queue %O", this.queuedValue);
this.queuedValue = void 0;
this.resolvers = withResolvers();
callbackPromise.then((output) => outputResolvers.resolve(output)).catch((error) => outputResolvers.reject(error));
} catch (error) {
this.queuedValue = void 0;
outputResolvers.reject(error);
}
}, timeMs, this.controller.signal);
return this.resolvers.promise;
}
};
var DebounceController = class {
throttleQueue;
queues = /* @__PURE__ */ new Map();
queuedQuerySync = createEmitter();
constructor(throttleQueue = new ThrottledQueue()) {
this.throttleQueue = throttleQueue;
}
useQueuedQueries(keys) {
return useSyncExternalStores(keys, (key, callback) => this.queuedQuerySync.on(key, callback), (key) => this.getQueuedQuery(key));
}
push(update, timeMs, adapter, processUrlSearchParams) {
if (!Number.isFinite(timeMs)) {
const getSnapshot = adapter.getSearchParamsSnapshot ?? getSearchParamsSnapshotFromLocation;
return Promise.resolve(getSnapshot());
}
const key = update.key;
if (!this.queues.has(key)) {
debug("[nuqs dqc] Creating debounce queue for `%s`", key);
const queue = new DebouncedPromiseQueue((update) => {
this.throttleQueue.push(update);
return this.throttleQueue.flush(adapter, processUrlSearchParams).finally(() => {
if (this.queues.get(update.key)?.queuedValue === void 0) {
debug("[nuqs dqc] Cleaning up empty queue for `%s`", update.key);
this.queues.delete(update.key);
}
this.queuedQuerySync.emit(update.key);
});
});
this.queues.set(key, queue);
}
debug("[nuqs dqc] Enqueueing debounce update %O", update);
const promise = this.queues.get(key).push(update, timeMs);
this.queuedQuerySync.emit(key);
return promise;
}
abort(key) {
const queue = this.queues.get(key);
if (!queue) return (passThrough) => passThrough;
debug("[nuqs dqc] Aborting debounce queue %s=%s", key, queue.queuedValue?.query);
this.queues.delete(key);
queue.abort();
this.queuedQuerySync.emit(key);
return (promise) => {
promise.then(queue.resolvers.resolve, queue.resolvers.reject);
return promise;
};
}
abortAll() {
for (const [key, queue] of this.queues.entries()) {
debug("[nuqs dqc] Aborting debounce queue %s=%s", key, queue.queuedValue?.query);
queue.abort();
queue.resolvers.resolve(new URLSearchParams());
this.queuedQuerySync.emit(key);
}
this.queues.clear();
}
getQueuedQuery(key) {
const debouncedQueued = this.queues.get(key)?.queuedValue?.query;
if (debouncedQueued !== void 0) return debouncedQueued;
return this.throttleQueue.getQueuedQuery(key);
}
};
const debounceController = new DebounceController(globalThrottleQueue);
//#endregion
export { write as a, throttle as c, isAbsentFromUrl as i, globalThrottleQueue as n, debounce as o, createEmitter as r, defaultRateLimit as s, debounceController as t };
//# sourceMappingURL=debounce-PSGthE_7.js.map