Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -79,7 +80,7 @@ interface Storage {
// Create cdeebee slice
export const cdeebeeSlice = factory<Storage>(
{
modules: ['history', 'listener', 'cancelation', 'storage'],
modules: ['history', 'listener', 'cancelation', 'storage', 'queryQueue'],
fileKey: 'file',
bodyKey: 'value',
listStrategy: {
Expand Down Expand Up @@ -164,7 +165,7 @@ The `factory` function accepts a settings object with the following options:

```typescript
interface CdeebeeSettings<T> {
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<T>; // Merge strategy per list: 'merge' | 'replace'
Expand Down Expand Up @@ -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<Storage>({
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:
Expand Down Expand Up @@ -386,6 +415,7 @@ export type {
CdeebeeRequestOptions,
CdeebeeValueList,
CdeebeeActiveRequest,
CdeebeeModule,
} from '@recats/cdeebee';
```

Expand Down
56 changes: 31 additions & 25 deletions example/request/app/components/demo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<header className='sticy top-0 margin-auto text-center p-3 w-full'>
<div className='flex items-center justify-center gap-2'>
<button onClick={() => dispatch(request({ api, method: 'POST', body: { pending: 5000, }, onResult: console.log }))} className={btn}>
Slow fetch
</button>
<button onClick={() => dispatch(request({ api, method: 'POST', body: { pending: 1000, }, onResult: console.log }))} className={btn}>
Fast fetch
</button>
</div>
</header>
{loading ? <p className='text-center text-red-600 font-bold'>Loading...</p> : <p>&nbsp;</p>}
<section className='grid grid-cols-3 gap-4'>
<article>
<h3 className='font-bold text-center'>cdeebee.Settings</h3>
<JsonView value={state.settings} collapsed={false} displayDataTypes={false} style={nordTheme} />
</article>
<article>
<h3 className='font-bold text-center'>cdeebee.request</h3>
<JsonView value={state.request} collapsed={false} displayDataTypes={false} style={nordTheme} />
</article>
<article>
<h3 className='font-bold text-center'>cdeebee.storage</h3>
<JsonView value={state.storage ?? {}} collapsed={false} displayDataTypes={false} style={nordTheme} />
</article>
</section>
<Header />

<div className='flex items-center justify-center gap-2 mb-4'>
<button onClick={() => dispatch(request({ api, method: 'POST', body: { pending: 5000, }, onResult: console.log }))} className={btn}>
Slow fetch
</button>
<button onClick={() => dispatch(request({ api, method: 'POST', body: { pending: 1000, }, onResult: console.log }))} className={btn}>
Fast fetch
</button>
</div>

<main className='max-w-6xl mx-auto p-4'>
<section className='grid grid-cols-3 gap-4'>
<article>
<h3 className='font-bold text-center mb-2'>cdeebee.Settings</h3>
<JsonView value={state.settings} collapsed={false} displayDataTypes={false} style={nordTheme} />
</article>
<article>
<h3 className='font-bold text-center mb-2'>cdeebee.request</h3>
<JsonView value={state.request} collapsed={false} displayDataTypes={false} style={nordTheme} />
</article>
<article>
<h3 className='font-bold text-center mb-2'>cdeebee.storage</h3>
<JsonView value={state.storage ?? {}} collapsed={false} displayDataTypes={false} style={nordTheme} />
</article>
</section>
</main>
</>
);
};
19 changes: 19 additions & 0 deletions example/request/app/components/header/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<header className='sticky top-0 z-10 border-b p-4 bg-black text-white'>
<div className='max-w-6xl mx-auto flex gap-4'>
<Link href='/' className={btn}>Home</Link>
<Link href='/queue' className={btn}>Query queue</Link>
</div>
</header>

<ModulesSettings />
</>
);
}
71 changes: 71 additions & 0 deletions example/request/app/components/modules-settings/index.tsx
Original file line number Diff line number Diff line change
@@ -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<CdeebeeModule, string> = {
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 (
<div className='p-4 mb-2'>
<div className='flex items-center justify-between gap-2'>
<h3 className='font-bold text-lg mb-3'>Modules</h3>
<button type='button' onClick={() => toggle(q => !q)} className='cursor-pointer'>toggle</button>
</div>

<div className={open ? '' : 'hidden'}>
{allModules.map(module => {
const isEnabled = enabledModuleList.includes(module);
return (
<label
key={module}
className={`flex items-center gap-3 p-3 rounded border-2 cursor-pointer transition-colors ${
isEnabled
? 'bg-green-50 border-green-300'
: 'bg-gray-50 border-gray-200'
}`}
>
<input
type='checkbox'
checked={isEnabled}
onChange={() => toggleModule(module)}
className='w-5 h-5 text-green-600 rounded focus:ring-2 focus:ring-green-500'
/>
<div className='flex-1'>
<div className='font-semibold text-gray-800'>
{module}
{isEnabled && (
<span className='ml-2 text-xs text-green-600 font-normal'>(enabled)</span>
)}
</div>
<div className='text-xs text-gray-600 mt-1'>{moduleDescriptions[module]}</div>
</div>
</label>
);
})}
</div>
</div>
);
}

154 changes: 154 additions & 0 deletions example/request/app/components/queue-demo/index.tsx
Original file line number Diff line number Diff line change
@@ -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<RequestStatus[]>([]);
const startTimeRef = useRef<number>(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 (
<>
<Header />

<main className='max-w-6xl mx-auto p-4'>
<div className='flex flex-col items-center gap-4 mb-8'>
<button onClick={sendSequentialRequests} className={btnQueue}>
Send 5 requests to queue (queryQueue)
</button>
{requests.length > 0 && (
<div className='mt-4 w-full p-4 bg-gray-50 rounded-lg border border-gray-200'>
<div className='font-bold mb-3 text-center text-lg'>Sequential Processing (queryQueue enabled)</div>
<div className='text-sm text-gray-600 mb-3 text-center'>
Note: Request #5 has shortest delay (200ms) but completes last due to queue order
</div>
<div className='space-y-2'>
{requests.map(req => (
<div key={req.id} className={`p-3 rounded border-2 ${
req.status === 'completed'
? 'bg-green-50 border-green-300'
: req.status === 'processing'
? 'bg-yellow-50 border-yellow-300'
: 'bg-gray-100 border-gray-300'
}`}>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-3'>
<span className={`font-bold text-lg ${
req.status === 'completed' ? 'text-green-700' :
req.status === 'processing' ? 'text-yellow-700' : 'text-gray-500'
}`}>
Request #{req.id}
</span>
<span className='text-sm text-gray-600'>
Delay: {req.delay}ms
</span>
<span className={`text-xs px-2 py-1 rounded ${
req.status === 'completed'
? 'bg-green-200 text-green-800'
: req.status === 'processing'
? 'bg-yellow-200 text-yellow-800'
: 'bg-gray-200 text-gray-600'
}`}>
{req.status === 'completed' ? '✓ Completed' :
req.status === 'processing' ? '⟳ Processing' : '⏳ Waiting'}
</span>
</div>
<div className='text-sm text-gray-600'>
{req.completed ? (
<span className='text-green-700 font-semibold'>
Completed at +{req.completed}ms
</span>
) : (
<span>Sent at +{req.sent}ms</span>
)}
</div>
</div>
{req.completed && (
<div className='mt-2 text-xs text-gray-500'>
Total time: {req.completed}ms (sent at +{req.sent}ms)
</div>
)}
</div>
))}
</div>
<div className='mt-4 p-2 bg-blue-50 rounded text-sm text-blue-800'>
<strong>Expected behavior:</strong> Requests complete in order 1→2→3→4→5,
even though #5 has the shortest delay. This proves sequential processing!
</div>
</div>
)}
</div>

{loading ? <p className='text-center text-red-600 font-bold'>Loading...</p> : <p>&nbsp;</p>}

<section className='grid grid-cols-3 gap-4'>
<article>
<h3 className='font-bold text-center mb-2'>cdeebee.Settings</h3>
<JsonView value={state.settings} collapsed={false} displayDataTypes={false} style={nordTheme} />
</article>
<article>
<h3 className='font-bold text-center mb-2'>cdeebee.request</h3>
<JsonView value={state.request} collapsed={false} displayDataTypes={false} style={nordTheme} />
</article>
<article>
<h3 className='font-bold text-center mb-2'>cdeebee.storage</h3>
<JsonView value={state.storage ?? {}} collapsed={false} displayDataTypes={false} style={nordTheme} />
</article>
</section>
</main>
</>
);
}

2 changes: 1 addition & 1 deletion example/request/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function RootLayout({ children }: Props) {
<StoreProvider>
<html lang='en'>
<body>
<main className='max-w-360 m-auto p-4'>
<main>
{children}
</main>
</body>
Expand Down
Loading