From d0ec5322b55511d1a4c418285baa7c77bdcd8efb Mon Sep 17 00:00:00 2001 From: senelway Date: Thu, 1 Jan 2026 17:29:15 +0000 Subject: [PATCH 1/2] [beta.7] add query queue --- example/request/app/components/demo/index.tsx | 56 ++-- .../request/app/components/header/index.tsx | 19 ++ .../app/components/modules-settings/index.tsx | 71 +++++ .../app/components/queue-demo/index.tsx | 154 +++++++++++ example/request/app/layout.tsx | 2 +- example/request/app/page.tsx | 2 +- example/request/app/queue/page.tsx | 11 + example/request/lib/store.ts | 25 +- example/request/pnpm-lock.yaml | 16 +- lib/index.ts | 2 +- lib/reducer/queryQueue.ts | 29 ++ lib/reducer/request.ts | 23 +- lib/reducer/types.ts | 2 +- package.json | 2 +- tests/lib/reducer/queryQueue.test.ts | 261 ++++++++++++++++++ 15 files changed, 627 insertions(+), 48 deletions(-) create mode 100644 example/request/app/components/header/index.tsx create mode 100644 example/request/app/components/modules-settings/index.tsx create mode 100644 example/request/app/components/queue-demo/index.tsx create mode 100644 example/request/app/queue/page.tsx create mode 100644 lib/reducer/queryQueue.ts create mode 100644 tests/lib/reducer/queryQueue.test.ts diff --git a/example/request/app/components/demo/index.tsx b/example/request/app/components/demo/index.tsx index d0cea83..7c90f92 100644 --- a/example/request/app/components/demo/index.tsx +++ b/example/request/app/components/demo/index.tsx @@ -4,41 +4,47 @@ import JsonView from '@uiw/react-json-view'; import { nordTheme } from '@uiw/react-json-view/nord'; import { useAppDispatch, useAppSelector } from '@/lib/hooks'; import useLoading from '@/app/hook/useLoading'; +import ModulesSettings from '../modules-settings'; +import Header from '../header'; const btn = 'px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 cursor:pointer'; const api = '/api/bundle'; + export default function Counter () { const state = useAppSelector(state => state.cdeebee); const loading = useLoading(api); const dispatch = useAppDispatch(); + return ( <> -
-
- - -
-
- {loading ?

Loading...

:

 

} -
-
-

cdeebee.Settings

- -
-
-

cdeebee.request

- -
-
-

cdeebee.storage

- -
-
+
+ +
+ + +
+ +
+
+
+

cdeebee.Settings

+ +
+
+

cdeebee.request

+ +
+
+

cdeebee.storage

+ +
+
+
); }; diff --git a/example/request/app/components/header/index.tsx b/example/request/app/components/header/index.tsx new file mode 100644 index 0000000..af0feeb --- /dev/null +++ b/example/request/app/components/header/index.tsx @@ -0,0 +1,19 @@ +import Link from 'next/link'; +import ModulesSettings from '../modules-settings'; + +const btn = 'px-4 py-2 rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 cursor:pointer'; + +export default function Header() { + return ( + <> +
+
+ Home + Query queue +
+
+ + + + ); +} diff --git a/example/request/app/components/modules-settings/index.tsx b/example/request/app/components/modules-settings/index.tsx new file mode 100644 index 0000000..660ce29 --- /dev/null +++ b/example/request/app/components/modules-settings/index.tsx @@ -0,0 +1,71 @@ +'use client'; +import { useAppDispatch, useAppSelector } from '@/lib/hooks'; +import type { CdeebeeModule } from '@recats/cdeebee'; +import { useState } from 'react'; + +const allModules: CdeebeeModule[] = ['history', 'listener', 'storage', 'cancelation', 'queryQueue']; + +const moduleDescriptions: Record = { + history: 'Tracks request history (done and errors)', + listener: 'Tracks active requests', + storage: 'Stores response data in state', + cancelation: 'Cancels previous requests to the same API', + queryQueue: 'Processes requests sequentially in queue order', +}; + +export default function ModulesSettings() { + const enabledModuleList = useAppSelector(state => state.cdeebee.settings.modules); + const dispatch = useAppDispatch(); + const [open, toggle] = useState(false); + + const toggleModule = (module: CdeebeeModule) => { + const currentModules = [...enabledModuleList] as string[]; + const moduleStr = module as string; + const index = currentModules.indexOf(moduleStr); + + const newModules: string[] = index === -1 ? [...currentModules, moduleStr] : currentModules.filter(m => m !== moduleStr); + dispatch({ type: 'cdeebee/setModules', payload: newModules, } ); + }; + + return ( +
+
+

Modules

+ +
+ +
+ {allModules.map(module => { + const isEnabled = enabledModuleList.includes(module); + return ( + + ); + })} +
+
+ ); +} + diff --git a/example/request/app/components/queue-demo/index.tsx b/example/request/app/components/queue-demo/index.tsx new file mode 100644 index 0000000..ddcc1cc --- /dev/null +++ b/example/request/app/components/queue-demo/index.tsx @@ -0,0 +1,154 @@ +'use client'; +import { request } from '@recats/cdeebee'; +import JsonView from '@uiw/react-json-view'; +import { nordTheme } from '@uiw/react-json-view/nord'; +import { useAppDispatch, useAppSelector } from '@/lib/hooks'; +import useLoading from '@/app/hook/useLoading'; +import { useState, useRef } from 'react'; +import Header from '../header'; + +const btnQueue = 'px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 cursor:pointer'; + +const api = '/api/bundle'; + +interface RequestStatus { + id: number; + sent: number; + completed?: number; + delay: number; + status: 'pending' | 'processing' | 'completed'; +} + +export default function QueueDemo() { + const state = useAppSelector(state => state.cdeebee); + const loading = useLoading(api); + const dispatch = useAppDispatch(); + const [requests, setRequests] = useState([]); + const startTimeRef = useRef(0); + + const sendSequentialRequests = () => { + startTimeRef.current = Date.now(); + setRequests([]); + + const delays = [1000, 800, 600, 400, 200]; + + delays.forEach((delay, index) => { + const requestNumber = index + 1; + const sentTime = Date.now() - startTimeRef.current; + + setRequests(prev => [...prev, { + id: requestNumber, + sent: sentTime, + delay, + status: 'pending' + }]); + + dispatch(request({ + api, + method: 'POST', + body: { pending: delay, requestId: requestNumber }, + onResult: () => { + const completedTime = Date.now() - startTimeRef.current; + setRequests(prev => prev.map(req => + req.id === requestNumber + ? { ...req, completed: completedTime, status: 'completed' } + : req.status === 'pending' ? { ...req, status: 'processing' } : req + )); + } + })); + }); + }; + + return ( + <> +
+ +
+
+ + {requests.length > 0 && ( +
+
Sequential Processing (queryQueue enabled)
+
+ Note: Request #5 has shortest delay (200ms) but completes last due to queue order +
+
+ {requests.map(req => ( +
+
+
+ + Request #{req.id} + + + Delay: {req.delay}ms + + + {req.status === 'completed' ? '✓ Completed' : + req.status === 'processing' ? '⟳ Processing' : '⏳ Waiting'} + +
+
+ {req.completed ? ( + + Completed at +{req.completed}ms + + ) : ( + Sent at +{req.sent}ms + )} +
+
+ {req.completed && ( +
+ Total time: {req.completed}ms (sent at +{req.sent}ms) +
+ )} +
+ ))} +
+
+ Expected behavior: Requests complete in order 1→2→3→4→5, + even though #5 has the shortest delay. This proves sequential processing! +
+
+ )} +
+ + {loading ?

Loading...

:

 

} + +
+
+

cdeebee.Settings

+ +
+
+

cdeebee.request

+ +
+
+

cdeebee.storage

+ +
+
+
+ + ); +} + diff --git a/example/request/app/layout.tsx b/example/request/app/layout.tsx index 7dc338c..3b553e0 100644 --- a/example/request/app/layout.tsx +++ b/example/request/app/layout.tsx @@ -12,7 +12,7 @@ export default function RootLayout({ children }: Props) { -
+
{children}
diff --git a/example/request/app/page.tsx b/example/request/app/page.tsx index 32757e7..bb41b4b 100644 --- a/example/request/app/page.tsx +++ b/example/request/app/page.tsx @@ -6,5 +6,5 @@ export default function IndexPage() { } export const metadata: Metadata = { - title: 'Redux Toolkit', + title: 'cdeebee home', }; diff --git a/example/request/app/queue/page.tsx b/example/request/app/queue/page.tsx new file mode 100644 index 0000000..77c418f --- /dev/null +++ b/example/request/app/queue/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next'; +import CdeebeeDemo from '../components/queue-demo'; + +export default function QueuePage() { + return ; +} + +export const metadata: Metadata = { + title: 'queue demo', +}; + diff --git a/example/request/lib/store.ts b/example/request/lib/store.ts index a2c85c3..59644dd 100644 --- a/example/request/lib/store.ts +++ b/example/request/lib/store.ts @@ -11,13 +11,14 @@ interface Storage { // Create cdeebee slice with default or custom initial state export const cdeebeeSlice = factory( { - modules: ['history', 'listener', 'cancelation', 'storage'], + modules: ['history', 'listener', 'storage', 'queryQueue'], fileKey: 'file', bodyKey: 'value', listStrategy: { bundleList: 'merge', campaignList: 'replace', }, + mergeWithHeaders: {}, mergeWithData: { sessionToken: '', }, @@ -27,7 +28,7 @@ export const cdeebeeSlice = factory( 123: { campaignID: 123, campaign: 'Holiday Campaign', timestamp: '2025-12-01T10:15:30.000Z' }, }, bundleList: { - 961: { t: 1 } as any, + 961: { t: 1 } as unknown as { bundleID: number; bundle: string; timestamp: string }, }, } ); @@ -44,7 +45,25 @@ export type RootState = ReturnType; // are needed for each request to prevent cross-request state pollution. export const makeStore = () => { return configureStore({ - reducer: rootReducer, + reducer: (state, action) => { + // Handle custom setModules action for demo + if (action.type === 'cdeebee/setModules' && 'payload' in action) { + const currentState = rootReducer(state, { type: '@@INIT' }); + if (currentState && 'cdeebee' in currentState) { + return { + ...currentState, + cdeebee: { + ...currentState.cdeebee, + settings: { + ...currentState.cdeebee.settings, + modules: action.payload as string[], + }, + }, + }; + } + } + return rootReducer(state, action); + }, // Adding the api middleware enables caching, invalidation, polling, // and other useful features of `rtk-query`. middleware: getDefaultMiddleware => { diff --git a/example/request/pnpm-lock.yaml b/example/request/pnpm-lock.yaml index 539692b..f0e0a8d 100644 --- a/example/request/pnpm-lock.yaml +++ b/example/request/pnpm-lock.yaml @@ -413,8 +413,8 @@ packages: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true - caniuse-lite@1.0.30001761: - resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} + caniuse-lite@1.0.30001762: + resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -433,8 +433,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - immer@11.1.0: - resolution: {integrity: sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==} + immer@11.1.3: + resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==} jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} @@ -799,7 +799,7 @@ snapshots: dependencies: '@standard-schema/spec': 1.1.0 '@standard-schema/utils': 0.3.0 - immer: 11.1.0 + immer: 11.1.3 redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.1 @@ -910,7 +910,7 @@ snapshots: baseline-browser-mapping@2.9.11: {} - caniuse-lite@1.0.30001761: {} + caniuse-lite@1.0.30001762: {} client-only@0.0.1: {} @@ -925,7 +925,7 @@ snapshots: graceful-fs@4.2.11: {} - immer@11.1.0: {} + immer@11.1.3: {} jiti@2.6.1: {} @@ -995,7 +995,7 @@ snapshots: '@next/env': 16.1.1 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.9.11 - caniuse-lite: 1.0.30001761 + caniuse-lite: 1.0.30001762 postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) diff --git a/lib/index.ts b/lib/index.ts index 724f7f4..c6c7eba 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,4 +1,4 @@ -export { type CdeebeeState, type CdeebeeValueList, type CdeebeeRequestOptions } from './reducer/types'; +export { type CdeebeeState, type CdeebeeValueList, type CdeebeeRequestOptions, type CdeebeeModule } from './reducer/types'; export { batchingUpdate } from './reducer/helpers'; export { request } from './reducer/request'; export { factory } from './reducer/index'; diff --git a/lib/reducer/queryQueue.ts b/lib/reducer/queryQueue.ts new file mode 100644 index 0000000..6f2b447 --- /dev/null +++ b/lib/reducer/queryQueue.ts @@ -0,0 +1,29 @@ +class QueryQueue { + private currentPromise: Promise = Promise.resolve(); + private queueLength = 0; + + async enqueue(task: () => Promise): Promise { + this.queueLength++; + + const previousPromise = this.currentPromise; + + this.currentPromise = previousPromise + .then(() => task(), () => task()) + .finally(() => { + this.queueLength--; + }); + + return this.currentPromise as Promise; + } + + getQueueLength(): number { + return this.queueLength; + } + + clear(): void { + this.queueLength = 0; + } +} + +export const queryQueue = new QueryQueue(); + diff --git a/lib/reducer/request.ts b/lib/reducer/request.ts index 7ac79d8..1fa4e2c 100644 --- a/lib/reducer/request.ts +++ b/lib/reducer/request.ts @@ -1,6 +1,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { checkModule } from './helpers'; import { abortManager } from './abortController'; +import { queryQueue } from './queryQueue'; import { type CdeebeeState, type CdeebeeRequestOptions } from './types'; export const request = createAsyncThunk( @@ -14,7 +15,8 @@ export const request = createAsyncThunk( checkModule(settings, 'cancelation', abort.init); - try { + const executeRequest = async () => { + try { const { method = 'POST', body, headers = {} } = options; const extraHeaders: Record = { ...(settings.mergeWithHeaders ?? {}), ...headers }; @@ -72,17 +74,24 @@ export const request = createAsyncThunk( if (withCallback) options.onResult!(result); return { result, startedAt, endedAt: new Date().toUTCString() }; - } catch (error) { - checkModule(settings, 'cancelation', abort.drop); + } catch (error) { + checkModule(settings, 'cancelation', abort.drop); + + if (withCallback) options.onResult!(error); - if (withCallback) options.onResult!(error); + if (error instanceof Error && error.name === 'AbortError') { + return rejectWithValue({ message: 'Request was cancelled', cancelled: true }); + } - if (error instanceof Error && error.name === 'AbortError') { - return rejectWithValue({ message: 'Request was cancelled', cancelled: true }); + return rejectWithValue({ message: error instanceof Error ? error.message : 'Unknown error occurred' }); } + }; - return rejectWithValue({ message: error instanceof Error ? error.message : 'Unknown error occurred' }); + if (settings.modules.includes('queryQueue')) { + return queryQueue.enqueue(executeRequest); } + + return executeRequest(); }, ); diff --git a/lib/reducer/types.ts b/lib/reducer/types.ts index 701ba8b..04351e1 100644 --- a/lib/reducer/types.ts +++ b/lib/reducer/types.ts @@ -1,4 +1,4 @@ -export type CdeebeeModule = 'history' | 'listener' | 'storage' | 'cancelation'; +export type CdeebeeModule = 'history' | 'listener' | 'storage' | 'cancelation' | 'queryQueue'; export type CdeebeeStrategy = 'merge' | 'replace'; export type CdeebeeListStrategy = Record; diff --git a/package.json b/package.json index 72eb25e..7da84bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@recats/cdeebee", - "version": "3.0.0-beta.6", + "version": "3.0.0-beta.7", "description": "React Redux data-logic library", "repository": "git@github.com:recats/cdeebee.git", "author": "recats", diff --git a/tests/lib/reducer/queryQueue.test.ts b/tests/lib/reducer/queryQueue.test.ts new file mode 100644 index 0000000..a5a8aa2 --- /dev/null +++ b/tests/lib/reducer/queryQueue.test.ts @@ -0,0 +1,261 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { queryQueue } from '../../../lib/reducer/queryQueue'; + +describe('queryQueue', () => { + beforeEach(() => { + queryQueue.clear(); + }); + + describe('sequential execution', () => { + it('should execute tasks sequentially in order', async () => { + const executionOrder: number[] = []; + + const task1 = async () => { + await new Promise(resolve => setTimeout(resolve, 50)); + executionOrder.push(1); + return 'task1'; + }; + + const task2 = async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + executionOrder.push(2); + return 'task2'; + }; + + const task3 = async () => { + await new Promise(resolve => setTimeout(resolve, 20)); + executionOrder.push(3); + return 'task3'; + }; + + const promise1 = queryQueue.enqueue(task1); + const promise2 = queryQueue.enqueue(task2); + const promise3 = queryQueue.enqueue(task3); + + const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]); + + expect(result1).toBe('task1'); + expect(result2).toBe('task2'); + expect(result3).toBe('task3'); + + expect(executionOrder).toEqual([1, 2, 3]); + }); + + it('should wait for previous task to complete before starting next', async () => { + const startTimes: number[] = []; + const endTimes: number[] = []; + + const task1 = async () => { + startTimes.push(Date.now()); + await new Promise(resolve => setTimeout(resolve, 100)); + endTimes.push(Date.now()); + return 'task1'; + }; + + const task2 = async () => { + startTimes.push(Date.now()); + await new Promise(resolve => setTimeout(resolve, 50)); + endTimes.push(Date.now()); + return 'task2'; + }; + + const promise1 = queryQueue.enqueue(task1); + const promise2 = queryQueue.enqueue(task2); + + await Promise.all([promise1, promise2]); + + expect(startTimes[1]).toBeGreaterThanOrEqual(endTimes[0]); + }); + }); + + describe('error handling', () => { + it('should continue processing queue even if a task fails', async () => { + const executionOrder: string[] = []; + + const task1 = async () => { + executionOrder.push('task1-start'); + throw new Error('Task 1 failed'); + }; + + const task2 = async () => { + executionOrder.push('task2-start'); + await new Promise(resolve => setTimeout(resolve, 10)); + executionOrder.push('task2-end'); + return 'task2-success'; + }; + + const task3 = async () => { + executionOrder.push('task3-start'); + return 'task3-success'; + }; + + const promise1 = queryQueue.enqueue(task1).catch(() => 'task1-error'); + const promise2 = queryQueue.enqueue(task2); + const promise3 = queryQueue.enqueue(task3); + + const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]); + + expect(result1).toBe('task1-error'); + + expect(result2).toBe('task2-success'); + expect(result3).toBe('task3-success'); + + expect(executionOrder).toEqual(['task1-start', 'task2-start', 'task2-end', 'task3-start']); + }); + + it('should handle rejected promises correctly', async () => { + const task1 = async () => { + throw new Error('Rejected'); + }; + + const task2 = async () => { + return 'success'; + }; + + const promise1 = queryQueue.enqueue(task1).catch(err => err.message); + const promise2 = queryQueue.enqueue(task2); + + const [result1, result2] = await Promise.all([promise1, promise2]); + + expect(result1).toBe('Rejected'); + expect(result2).toBe('success'); + }); + }); + + describe('queue length', () => { + it('should track queue length correctly', async () => { + expect(queryQueue.getQueueLength()).toBe(0); + + const task1 = queryQueue.enqueue(async () => { + await new Promise(resolve => setTimeout(resolve, 50)); + return 'task1'; + }); + + expect(queryQueue.getQueueLength()).toBeGreaterThan(0); + + const task2 = queryQueue.enqueue(async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + return 'task2'; + }); + + expect(queryQueue.getQueueLength()).toBeGreaterThan(1); + + await Promise.all([task1, task2]); + + expect(queryQueue.getQueueLength()).toBe(0); + }); + + it('should decrease queue length after task completion', async () => { + const task1 = queryQueue.enqueue(async () => { + await new Promise(resolve => setTimeout(resolve, 30)); + return 'task1'; + }); + + const initialLength = queryQueue.getQueueLength(); + expect(initialLength).toBe(1); + + await task1; + + expect(queryQueue.getQueueLength()).toBe(0); + }); + }); + + describe('clear', () => { + it('should reset queue length to 0', () => { + queryQueue.enqueue(async () => 'task1'); + queryQueue.enqueue(async () => 'task2'); + + expect(queryQueue.getQueueLength()).toBeGreaterThan(0); + + queryQueue.clear(); + + expect(queryQueue.getQueueLength()).toBe(0); + }); + + it('should not affect currently executing tasks', async () => { + const executionLog: string[] = []; + + const task1 = queryQueue.enqueue(async () => { + executionLog.push('task1-start'); + await new Promise(resolve => setTimeout(resolve, 50)); + executionLog.push('task1-end'); + return 'task1'; + }); + + queryQueue.clear(); + + await task1; + + expect(executionLog).toEqual(['task1-start', 'task1-end']); + }); + }); + + describe('return values', () => { + it('should return correct values from tasks', async () => { + const task1 = queryQueue.enqueue(async () => 42); + const task2 = queryQueue.enqueue(async () => 'hello'); + const task3 = queryQueue.enqueue(async () => ({ key: 'value' })); + + const [result1, result2, result3] = await Promise.all([task1, task2, task3]); + + expect(result1).toBe(42); + expect(result2).toBe('hello'); + expect(result3).toEqual({ key: 'value' }); + }); + }); + + describe('multiple rapid enqueues', () => { + it('should handle many tasks enqueued rapidly', async () => { + const promises: Promise[] = []; + + for (let i = 0; i < 10; i++) { + const taskNumber = i; + promises.push( + queryQueue.enqueue(async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + return taskNumber; + }) + ); + } + + const allResultList = await Promise.all(promises); + + expect(allResultList).toHaveLength(10); + + expect(allResultList).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); + }); + + describe('real-world scenario: sequential API calls', () => { + it('should process API-like requests sequentially', async () => { + const callOrder: number[] = []; + const responses: string[] = []; + + const makeRequest = (id: number, delay: number) => { + return queryQueue.enqueue(async () => { + callOrder.push(id); + await new Promise(resolve => setTimeout(resolve, delay)); + const response = `Response-${id}`; + responses.push(response); + return response; + }); + }; + + const request1 = makeRequest(1, 100); + const request2 = makeRequest(2, 50); // Faster but should wait + const request3 = makeRequest(3, 30); + const request4 = makeRequest(4, 20); + const request5 = makeRequest(5, 10); // Fastest but should be last + + const resultList = await Promise.all([request1, request2, request3, request4, request5]); + + expect(resultList).toHaveLength(5); + expect(resultList).toEqual(['Response-1', 'Response-2', 'Response-3', 'Response-4', 'Response-5']); + + expect(callOrder).toEqual([1, 2, 3, 4, 5]); + + expect(responses).toEqual(['Response-1', 'Response-2', 'Response-3', 'Response-4', 'Response-5']); + }); + }); +}); + From 9349dbf496c541d7c27cef48c77e5dfa0ea828b3 Mon Sep 17 00:00:00 2001 From: senelway Date: Thu, 1 Jan 2026 17:50:17 +0000 Subject: [PATCH 2/2] update readme --- README.md | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f58c5d2..293b0b8 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ cdeebee uses a modular architecture with the following modules: - **`history`**: Tracks request history (successful and failed requests) - **`listener`**: Tracks active requests for loading states - **`cancelation`**: Manages request cancellation (automatically cancels previous requests to the same API) +- **`queryQueue`**: Processes requests sequentially in the order they were sent, ensuring they complete and are stored in the correct sequence ## Quick Start @@ -79,7 +80,7 @@ interface Storage { // Create cdeebee slice export const cdeebeeSlice = factory( { - modules: ['history', 'listener', 'cancelation', 'storage'], + modules: ['history', 'listener', 'cancelation', 'storage', 'queryQueue'], fileKey: 'file', bodyKey: 'value', listStrategy: { @@ -164,7 +165,7 @@ The `factory` function accepts a settings object with the following options: ```typescript interface CdeebeeSettings { - modules: CdeebeeModule[]; // Active modules: 'history' | 'listener' | 'storage' | 'cancelation' + modules: CdeebeeModule[]; // Active modules: 'history' | 'listener' | 'storage' | 'cancelation' | 'queryQueue' fileKey: string; // Key name for file uploads in FormData bodyKey: string; // Key name for request body in FormData listStrategy?: CdeebeeListStrategy; // Merge strategy per list: 'merge' | 'replace' @@ -326,6 +327,34 @@ dispatch(request({ api: '/api/data', body: { query: 'slow' } })); dispatch(request({ api: '/api/data', body: { query: 'fast' } })); ``` +### Sequential Request Processing (queryQueue) + +When the `queryQueue` module is enabled, all requests are processed sequentially in the order they were sent. This ensures that: + +- Requests complete in the exact order they were dispatched +- Data is stored in the store in the correct sequence +- Even if a faster request is sent after a slower one, it will wait for the previous request to complete + +This is particularly useful when you need to maintain data consistency and ensure that updates happen in the correct order. + +```typescript +// Enable queryQueue module +const cdeebeeSlice = factory({ + modules: ['history', 'listener', 'storage', 'queryQueue'], + // ... other settings +}); + +// Send multiple requests - they will be processed sequentially +dispatch(request({ api: '/api/data', body: { id: 1 } })); // Completes first +dispatch(request({ api: '/api/data', body: { id: 2 } })); // Waits for #1, then completes +dispatch(request({ api: '/api/data', body: { id: 3 } })); // Waits for #2, then completes + +// Even if request #3 is faster, it will still complete last +// All requests are stored in the store in order: 1 → 2 → 3 +``` + +**Note:** The `queryQueue` module processes requests sequentially across all APIs. If you need parallel processing for different APIs, you would need separate cdeebee instances or disable the module for those specific requests. + ### Manual State Updates You can manually update the storage using the `set` action: @@ -386,6 +415,7 @@ export type { CdeebeeRequestOptions, CdeebeeValueList, CdeebeeActiveRequest, + CdeebeeModule, } from '@recats/cdeebee'; ```