react-p-queue
Version:
A React library for handling promise-based queue management with support for batch processing, throttling, and concurrency control using `p-queue`
289 lines (285 loc) • 7.5 kB
JavaScript
"use client";
// index.tsx
import assert from "assert";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useReducer
} from "react";
// batch.ts
var BatchQueue = class {
batchSize;
throttleTime;
resolver;
identifier;
buffer;
timeout;
/**
* Creates an instance of BatchQueue.
*
* @param {Object} params - Configuration parameters for the batch queue.
* @param {number} params.batchSize - The maximum size of the batch before triggering a flush.
* @param {number} params.throttleTime - The maximum time (in milliseconds) to wait before triggering a flush.
* @param {(batch: T[]) => Promise<R[]>} params.resolver - The function to call with the batch of tasks.
* @param {keyof R} params.identifier - The property in the resolver result to match with the task.
*/
constructor({
batchSize,
throttleTime,
resolver,
identifier
}) {
this.batchSize = batchSize;
this.throttleTime = throttleTime;
this.resolver = resolver;
this.identifier = identifier;
this.buffer = [];
this.timeout = null;
}
/**
* Adds a task to the batch queue. If the batch size is reached, it triggers an immediate flush.
* Otherwise, it waits for the specified throttle time before flushing the remaining tasks.
*
* @param {T} task - The task to be added to the batch queue.
* @returns {Promise<R>} A promise that resolves with the result of the task once handled.
*/
addTask(task) {
return new Promise((resolve, reject) => {
this.buffer.push({ task, resolve, reject });
if (this.buffer.length >= this.batchSize) {
this.flush();
} else {
if (!this.timeout) {
this.timeout = setTimeout(() => this.flush(), this.throttleTime);
}
}
});
}
/**
* Flushes the current batch of tasks. This will clear the buffer and call the resolver function
* with the current batch. If a timeout was set, it will be cleared.
*
* @returns {void}
*/
flush() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (this.buffer.length > 0) {
const batch = this.buffer.splice(0, this.batchSize);
this.processBatch(batch);
}
}
/**
* Processes a batch of tasks concurrently.
*
* @param {Array} batch - The batch of tasks to process.
* @returns {void}
*/
async processBatch(batch) {
try {
const results = await this.resolver(batch.map((item) => item.task));
results.forEach((result) => {
const matchedTask = batch.find(
(item) => (
// @ts-expect-error
item.task[this.identifier] === result[this.identifier]
)
);
if (matchedTask) {
matchedTask.resolve(result);
}
});
} catch (error) {
batch.forEach((item) => item.reject(error));
}
}
};
var batch_default = BatchQueue;
// index.tsx
import { jsx } from "react/jsx-runtime";
var PQueueResolverContext = createContext(
null
);
var PQueueContext = createContext(null);
function initstate(config) {
return {
config,
store: [],
tasks: []
};
}
var __noop = () => {
};
var PQueueDispatchContext = createContext(__noop);
function reducer(state, action) {
switch (action.type) {
case "add": {
return {
...state,
tasks: [...state.tasks, action.task]
};
}
case "start": {
break;
}
case "result": {
const { result } = action;
const { error } = result;
return {
...state,
store: [...state.store, { ...result, ok: error ? true : false }]
};
}
case "clear":
return {
...state,
store: [],
tasks: []
};
}
return state;
}
function QueueProvider({
config,
resolver,
batch,
throttle,
queue,
children
}) {
const [state, dispatch] = useReducer(reducer, initstate(config));
const pQueue = useMemo(() => queue, [queue]);
const batchqueue = useMemo(() => {
if (!batch) return null;
return new batch_default({
batchSize: batch,
throttleTime: throttle,
identifier: config.identifier,
resolver: async (tasks) => {
var _a;
const results = await resolver(...tasks);
(_a = results.data) == null ? void 0 : _a.forEach((result, index) => {
const task = tasks[index];
dispatch({
type: "result",
result: { data: result, error: results.error }
});
});
if (!results.data) {
throw results.error || new Error("Resolver failed");
}
return results.data;
}
});
}, [batch, resolver, throttle, config.identifier, dispatch]);
useEffect(() => {
if (!batchqueue) return;
const originalFlush = batchqueue.flush.bind(batchqueue);
batchqueue.flush = () => {
pQueue.add(async () => {
await originalFlush();
});
};
return () => {
batchqueue.flush();
};
}, [batchqueue, pQueue]);
return /* @__PURE__ */ jsx(
PQueueResolverContext.Provider,
{
value: {
resolver,
queue: pQueue,
batch: batchqueue
},
children: /* @__PURE__ */ jsx(PQueueContext.Provider, { value: state, children: /* @__PURE__ */ jsx(PQueueDispatchContext.Provider, { value: dispatch, children }) })
}
);
}
function __useDispatch() {
const dispatch = useContext(PQueueDispatchContext);
if (!dispatch) throw new Error("QueueProvider not provided");
return dispatch;
}
function __useResolver() {
const context = useContext(PQueueResolverContext);
if (!context) throw new Error("QueueProvider not provided");
return {
batch: context.batch,
resolver: context.resolver,
queue: context.queue
};
}
function useQueueStore() {
const state = useContext(PQueueContext);
if (!state) throw new Error("QueueProvider not provided");
const { store } = state;
return store;
}
function useQueue() {
const dispatch = __useDispatch();
const { resolver, queue, batch } = __useResolver();
const state = useContext(PQueueContext);
const store = useQueueStore();
if (!state) throw new Error("QueueProvider not provided");
const onAdd = useCallback(
async (task) => {
const existing = store.find((s) => {
return s.data && // @ts-ignore
s.data[state.config.identifier] === task[state.config.identifier];
});
if (existing) {
return existing;
}
if (batch) {
const res = await batch.addTask(task);
if (res) {
return {
data: res,
error: null
};
}
return {
data: null,
error: new Error("Resolver failed")
};
} else {
try {
const res = await queue.add(() => resolver(task));
assert(res);
dispatch({ type: "result", result: res });
return res;
} catch (e) {
return {
data: null,
error: e
};
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[batch, dispatch, resolver, queue]
);
const onClear = useCallback(() => {
dispatch({ type: "clear" });
queue.clear();
}, [dispatch, queue]);
return useMemo(
() => ({
add: onAdd,
clear: onClear
}),
[onAdd, onClear]
);
}
var index_default = QueueProvider;
export {
index_default as default,
useQueue,
useQueueStore
};