use-abcd 0.2.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +459 -85
  2. package/dist/App.d.ts +2 -0
  3. package/dist/cache.d.ts +12 -0
  4. package/dist/cache.test.d.ts +1 -0
  5. package/dist/chunks/client-VrsFvEIA.js +144 -0
  6. package/dist/chunks/client-VrsFvEIA.js.map +1 -0
  7. package/dist/chunks/types-Dy4rYb2N.js +19 -0
  8. package/dist/chunks/types-Dy4rYb2N.js.map +1 -0
  9. package/dist/collection.d.ts +54 -0
  10. package/dist/collection.e2e.test.d.ts +1 -0
  11. package/dist/examples/OptimisticComments.d.ts +2 -0
  12. package/dist/examples/PaginatedUsers.d.ts +2 -0
  13. package/dist/examples/Products.d.ts +2 -0
  14. package/dist/fetch-handler.d.ts +34 -0
  15. package/dist/fetch-handler.test.d.ts +1 -0
  16. package/dist/index.d.ts +14 -0
  17. package/dist/index.js +2283 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/item.d.ts +21 -0
  20. package/dist/main.d.ts +1 -0
  21. package/dist/mocks/browser.d.ts +1 -0
  22. package/dist/mocks/handlers.d.ts +2 -0
  23. package/dist/runtime/client.d.ts +51 -0
  24. package/dist/runtime/client.js +16 -0
  25. package/dist/runtime/client.js.map +1 -0
  26. package/dist/runtime/client.test.d.ts +1 -0
  27. package/dist/runtime/index.d.ts +5 -0
  28. package/dist/runtime/server.d.ts +33 -0
  29. package/dist/runtime/server.js +121 -0
  30. package/dist/runtime/server.js.map +1 -0
  31. package/dist/runtime/server.test.d.ts +1 -0
  32. package/dist/runtime/types.d.ts +70 -0
  33. package/dist/sync-queue.d.ts +29 -0
  34. package/dist/sync-queue.test.d.ts +1 -0
  35. package/dist/types.d.ts +61 -0
  36. package/dist/useCrud.d.ts +28 -169
  37. package/dist/useItem.d.ts +11 -0
  38. package/dist/utils.d.ts +4 -0
  39. package/package.json +25 -7
  40. package/dist/useCrud.js +0 -2135
package/README.md CHANGED
@@ -8,13 +8,13 @@ A powerful React hook for managing ABCD (or CRUD) operations with optimistic upd
8
8
 
9
9
  ## Features
10
10
 
11
- - 🔄 Automatic state management
12
- - ⚡ Optimistic updates
13
- - 🗄️ Built-in caching
14
- - 🎯 Type-safe
15
- - 🚫 Automatic error handling
16
- - Debounce support
17
- - 🔍 Request cancellation
11
+ - 🔄 Automatic state management with React 19 compatible hooks
12
+ - ⚡ Optimistic updates for instant UI feedback
13
+ - 🗄️ Built-in caching with configurable TTL and capacity
14
+ - 🎯 Type-safe with full TypeScript support
15
+ - Debounced sync with configurable delays
16
+ - 🔄 Sync queue management with pause/resume/retry
17
+ - 🎨 Context-based filtering and pagination
18
18
 
19
19
  ## Installation
20
20
 
@@ -26,103 +26,477 @@ yarn add use-abcd
26
26
  bun add use-abcd
27
27
  ```
28
28
 
29
- ## Quick Example
29
+ ## Package Exports
30
+
31
+ The package provides multiple entry points:
32
+
33
+ ```typescript
34
+ // Main package - React hooks and client-side sync utilities
35
+ import { useCrud, Collection, createSyncClient } from "use-abcd";
36
+
37
+ // Runtime client - Client & server sync utilities (for isomorphic code)
38
+ import { createSyncClient, createSyncServer } from "use-abcd/runtime/client";
39
+
40
+ // Runtime server - Server-side sync utilities only
41
+ import { createSyncServer, serverSyncSuccess } from "use-abcd/runtime/server";
42
+ ```
43
+
44
+ ## Quick Start
30
45
 
31
46
  ```typescript
32
- // biome-ignore assist/source/organizeImports: useless
33
- import React, { useCallback } from "react";
34
- import { useCrud, useItemState, type CrudConfig, type ItemWithState } from "../useCrud";
35
- import { map } from "lodash-es";
36
- import { wait } from "../utils";
37
-
38
- type Todo = {
39
- userId: string;
47
+ import { useCrud, type Config } from "use-abcd";
48
+
49
+ interface Todo {
40
50
  id: string;
41
51
  title: string;
42
52
  completed: boolean;
43
- };
53
+ }
44
54
 
45
- const TodoCrud: CrudConfig<Todo> = {
46
- id: "todo-crud",
47
- context: {},
48
- caching: {
49
- age: 5000000,
50
- capacity: 10,
51
- },
52
- fetch: async () => {
53
- return {
54
- items: await fetch("https://jsonplaceholder.typicode.com/todos")
55
- .then((r) => r.json())
56
- .then((items) => items.slice(0, 10)),
57
- metadata: {},
58
- };
55
+ const TodoConfig: Config<Todo, {}> = {
56
+ id: "todos",
57
+ initialContext: {},
58
+ getId: (item) => item.id,
59
+
60
+ onFetch: async (context, signal) => {
61
+ const response = await fetch("/api/todos", { signal });
62
+ const data = await response.json();
63
+ return data.items;
59
64
  },
60
- update: async (item, { signal }) => {
61
- await wait(1000, signal);
62
- return { id: item.id };
65
+
66
+ onSync: async (changes, signal) => {
67
+ // Handle create, update, delete operations
68
+ const results = [];
69
+ for (const change of changes) {
70
+ // Process each change and return results
71
+ results.push({ id: change.id, status: "success" });
72
+ }
73
+ return results;
63
74
  },
64
75
  };
65
76
 
66
- const Item = React.memo(function Item(props: { item: ItemWithState<Todo> }) {
67
- const { item } = props;
68
- const [data, { update, states }] = useItemState("todo-crud", item);
77
+ function TodoList() {
78
+ const { items, loading, create, update, remove } = useCrud(TodoConfig);
79
+
80
+ // Use items, create, update, remove in your UI
81
+ }
82
+ ```
83
+
84
+ ## Core Concepts
85
+
86
+ ### Config
87
+
88
+ The `Config` object defines how your data is fetched, synced, and managed:
89
+
90
+ ```typescript
91
+ type Config<T, C> = {
92
+ id: string; // Unique identifier for this collection
93
+ initialContext: C; // Initial context (filters, pagination, etc.)
94
+ getId: (item: T) => string; // Extract ID from item
95
+
96
+ // Optional sync configuration
97
+ syncDebounce?: number; // Debounce delay for sync (default: 300ms)
98
+ syncRetries?: number; // Max retry attempts (default: 3)
99
+
100
+ // Optional cache configuration
101
+ cacheCapacity?: number; // Max cache entries (default: 10)
102
+ cacheTtl?: number; // Cache TTL in ms (default: 60000)
103
+
104
+ // Required handlers
105
+ onFetch: (context: C, signal: AbortSignal) => Promise<T[]>;
106
+ onSync: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
107
+ };
108
+ ```
109
+
110
+ ### Hook API
111
+
112
+ The `useCrud` hook returns:
113
+
114
+ ```typescript
115
+ {
116
+ // State
117
+ items: Map<string, T>; // Current items
118
+ context: C; // Current context
119
+ loading: boolean; // Fetch loading state
120
+ syncing: boolean; // Sync in progress
121
+ syncQueue: SyncQueueState<T>; // Sync queue state
122
+ syncState: SyncState; // Overall sync state
123
+
124
+ // Item operations (optimistic)
125
+ create: (item: T) => void;
126
+ update: (id: string, mutate: (draft: T) => void) => void;
127
+ remove: (id: string) => void;
128
+ getItem: (id: string) => Item<T, C>;
129
+ getItemStatus: (id: string) => ItemStatus | null;
130
+
131
+ // Context & refresh
132
+ setContext: (mutate: (draft: C) => void) => void;
133
+ refresh: () => Promise<void>;
134
+
135
+ // Sync controls
136
+ pauseSync: () => void;
137
+ resumeSync: () => void;
138
+ retrySync: (id?: string) => void;
139
+ }
140
+ ```
141
+
142
+ ## Examples
143
+
144
+ The repository includes comprehensive examples demonstrating various use cases:
145
+
146
+ ### 1. Full CRUD Operations (Products Example)
147
+
148
+ Demonstrates complete CRUD functionality with:
149
+ - Create, read, update, delete operations
150
+ - Category filtering and search
151
+ - Sync queue management
152
+ - Error handling with retries
153
+ - Per-item status indicators
154
+
155
+ ```typescript
156
+ const ProductsConfig: Config<Product, ProductContext> = {
157
+ id: "products",
158
+ initialContext: { page: 1, limit: 10 },
159
+ getId: (item) => item.id,
69
160
 
70
- const markComplete = useCallback(() => {
71
- update((draft) => {
72
- draft.completed = !data.completed;
161
+ onFetch: async (context, signal) => {
162
+ const params = new URLSearchParams({
163
+ page: String(context.page),
164
+ limit: String(context.limit),
73
165
  });
74
- }, [update, data]);
75
-
76
- return (
77
- <div key={data.id} className="flex justify-between gap-2 mb-1 min-w-[500px]">
78
- <div
79
- className={
80
- data.completed ? "line-through font-bold text-gray-700" : "font-bold text-gray-700"
81
- }
82
- >
83
- {data.title}
84
- </div>
85
- <button
86
- type="button"
87
- className="bg-blue-300 px-2 rounded active:bg-blue-400 cursor-pointer font-bold disabled:opacity-40"
88
- onClick={markComplete}
89
- disabled={states.has("update")}
90
- >
91
- {states.has("update")
92
- ? "Updating..."
93
- : data.completed
94
- ? "Mark incomplete"
95
- : "Mark complete"}
96
- </button>
97
- </div>
98
- );
166
+ if (context.category) params.append("category", context.category);
167
+ if (context.search) params.append("search", context.search);
168
+
169
+ const response = await fetch(`/api/products?${params}`, { signal });
170
+ return (await response.json()).items;
171
+ },
172
+
173
+ onSync: async (changes, signal) => {
174
+ // Handle batch sync operations
175
+ },
176
+ };
177
+ ```
178
+
179
+ **Key features:**
180
+ - Filtering by category
181
+ - Text search
182
+ - Pause/resume sync
183
+ - Retry failed operations
184
+ - Visual status indicators
185
+
186
+ ### 2. Pagination (Users Example)
187
+
188
+ Shows context-based pagination with:
189
+ - Dynamic page size selection
190
+ - Next/previous navigation
191
+ - Context updates trigger re-fetch
192
+
193
+ ```typescript
194
+ interface UserContext {
195
+ page: number;
196
+ limit: number;
197
+ }
198
+
199
+ const { items, context, setContext } = useCrud<User, UserContext>(UsersConfig);
200
+
201
+ // Change page
202
+ setContext((draft) => {
203
+ draft.page += 1;
99
204
  });
100
205
 
101
- export const Todo = React.memo(function Todo() {
102
- const {
103
- items,
104
- fetchState: { isLoading },
105
- } = useCrud(TodoCrud);
206
+ // Change items per page
207
+ setContext((draft) => {
208
+ draft.limit = 20;
209
+ draft.page = 1; // Reset to first page
210
+ });
211
+ ```
106
212
 
107
- if (isLoading) {
108
- return <div>Loading...</div>;
109
- }
213
+ **Key features:**
214
+ - Configurable page size
215
+ - Context-based pagination
216
+ - Automatic re-fetch on context change
217
+
218
+ ### 3. Optimistic Updates (Comments Example)
219
+
220
+ Demonstrates the power of optimistic updates:
221
+ - Instant UI feedback
222
+ - Background synchronization
223
+ - Sync queue visualization
224
+ - Manual retry controls
225
+ - Error state handling
226
+
227
+ ```typescript
228
+ const CommentsConfig: Config<Comment, CommentContext> = {
229
+ id: "comments-optimistic",
230
+ initialContext: { postId: "1" },
231
+ getId: (item) => item.id,
232
+ syncDebounce: 100, // Very short debounce for demo
233
+
234
+ // ... handlers
235
+ };
236
+
237
+ // Create appears instantly in UI
238
+ create({
239
+ id: `temp-${Date.now()}`,
240
+ text: "New comment",
241
+ author: "You",
242
+ createdAt: new Date().toISOString(),
243
+ });
244
+ ```
245
+
246
+ **Key features:**
247
+ - Immediate UI updates
248
+ - Sync queue status display
249
+ - Pause/resume synchronization
250
+ - Per-item sync status
251
+ - Manual retry for errors
252
+
253
+ ### 4. Original Examples
254
+
255
+ The repository also includes the original simpler examples:
256
+ - **Blog Post**: Single item editing with optimistic updates
257
+ - **Todo List**: Simple list with toggle completion
258
+
259
+ ## Advanced Usage
260
+
261
+ ### Custom Context for Filtering
262
+
263
+ Use context to manage filters, pagination, sorting, etc.:
264
+
265
+ ```typescript
266
+ interface ProductContext {
267
+ page: number;
268
+ limit: number;
269
+ category?: string;
270
+ search?: string;
271
+ sortBy?: "name" | "price";
272
+ }
273
+
274
+ const { context, setContext } = useCrud<Product, ProductContext>(config);
275
+
276
+ // Update multiple context fields
277
+ setContext((draft) => {
278
+ draft.category = "electronics";
279
+ draft.page = 1;
280
+ });
281
+ ```
282
+
283
+ ### Monitoring Sync Queue
284
+
285
+ Track pending changes and errors:
286
+
287
+ ```typescript
288
+ const { syncQueue, pauseSync, resumeSync, retrySync } = useCrud(config);
110
289
 
111
- return (
112
- <div>
113
- <h2 className="font-bold text-3xl mt-4">Todo with useCrud()</h2>
114
- <div className="p-2">
115
- {map(items, (item) => (
116
- <Item key={item.data.id} item={item} />
117
- ))}
118
- </div>
119
- </div>
120
- );
290
+ console.log({
291
+ pending: syncQueue.queue.size,
292
+ inFlight: syncQueue.inFlight.size,
293
+ errors: syncQueue.errors.size,
294
+ isPaused: syncQueue.isPaused,
295
+ isSyncing: syncQueue.isSyncing,
121
296
  });
297
+
298
+ // Pause sync temporarily
299
+ pauseSync();
300
+
301
+ // Resume sync
302
+ resumeSync();
303
+
304
+ // Retry specific item
305
+ retrySync(itemId);
306
+
307
+ // Retry all failed items
308
+ retrySync();
309
+ ```
310
+
311
+ ### Per-Item Status
312
+
313
+ Track the sync status of individual items:
314
+
315
+ ```typescript
316
+ const { getItemStatus } = useCrud(config);
317
+
318
+ const status = getItemStatus(itemId);
319
+ if (status) {
320
+ console.log({
321
+ type: status.type, // "create" | "update" | "delete"
322
+ status: status.status, // "pending" | "syncing" | "success" | "error"
323
+ retries: status.retries, // Number of retry attempts
324
+ error: status.error, // Error message if failed
325
+ });
326
+ }
327
+ ```
328
+
329
+ ### ID Remapping for Optimistic Creates
330
+
331
+ When creating items optimistically, you typically use a temporary ID (e.g., `temp-${Date.now()}`). After the server confirms the creation, it may assign a different permanent ID. The library automatically handles this ID remapping.
332
+
333
+ **In your `onSync` handler**, return the server-assigned `newId` for create operations:
334
+
335
+ ```typescript
336
+ onSync: async (changes, signal) => {
337
+ const results: SyncResult[] = [];
338
+
339
+ for (const change of changes) {
340
+ if (change.type === "create") {
341
+ const response = await fetch("/api/items", {
342
+ method: "POST",
343
+ headers: { "Content-Type": "application/json" },
344
+ body: JSON.stringify(change.data),
345
+ signal,
346
+ });
347
+
348
+ if (!response.ok) throw new Error("Failed to create");
349
+
350
+ const data = await response.json();
351
+ // Return newId to remap the temporary ID to the server-assigned ID
352
+ results.push({
353
+ id: change.id, // The temporary ID
354
+ status: "success",
355
+ newId: data.id, // The server-assigned permanent ID
356
+ });
357
+ }
358
+ // ... handle update and delete
359
+ }
360
+
361
+ return results;
362
+ };
363
+ ```
364
+
365
+ **What happens automatically:**
366
+ 1. The item's key in the `items` Map is updated from `temp-123` to `server-456`
367
+ 2. The item's `id` property is updated (assumes item has an `id` field)
368
+ 3. Any `Item` references are updated to use the new ID
369
+ 4. The UI re-renders with the correct permanent ID
370
+
371
+ **Custom ID field**: If your item uses a different property for the ID (not `id`), provide a `setId` function in your config:
372
+
373
+ ```typescript
374
+ const config: Config<MyItem, Context> = {
375
+ getId: (item) => item.itemId,
376
+ setId: (item, newId) => ({ ...item, itemId: newId }),
377
+ // ...
378
+ };
379
+ ```
380
+
381
+ ### Cache Control
382
+
383
+ Control caching behavior:
384
+
385
+ ```typescript
386
+ const config: Config<T, C> = {
387
+ // ...
388
+ cacheCapacity: 20, // Store up to 20 cache entries
389
+ cacheTtl: 30000, // Cache expires after 30 seconds
390
+ };
391
+
392
+ const { refresh } = useCrud(config);
393
+
394
+ // Force refresh (bypass cache)
395
+ await refresh();
122
396
  ```
123
397
 
124
- > **Note**: This is a single-file library with a focused scope. Please read the source code for a deeper understanding of its implementation and capabilities.
398
+ ## Running Examples Locally
399
+
400
+ The repository includes a development environment with MSW (Mock Service Worker) for testing:
401
+
402
+ ```bash
403
+ # Clone the repository
404
+ git clone https://github.com/smtrd3/use-abcd
405
+ cd use-abcd
406
+
407
+ # Install dependencies
408
+ bun install # or npm install
409
+
410
+ # Start development server
411
+ bun run dev # or npm run dev
412
+ ```
413
+
414
+ Visit `http://localhost:5173` to see the examples in action.
415
+
416
+ ### Available Examples:
417
+
418
+ 1. **Products (Full CRUD)** - Complete CRUD operations with filtering
419
+ 2. **Pagination** - Context-based pagination with users
420
+ 3. **Optimistic Updates** - Comments with sync queue visualization
421
+ 4. **Blog Post (Original)** - Simple single-item editing
422
+ 5. **Todo (Original)** - Basic list operations
423
+
424
+ ## API Reference
425
+
426
+ ### Types
427
+
428
+ ```typescript
429
+ // Main configuration
430
+ type Config<T, C> = {
431
+ id: string;
432
+ initialContext: C;
433
+ getId: (item: T) => string;
434
+ setId?: (item: T, newId: string) => T; // Optional: for ID remapping on create
435
+ syncDebounce?: number;
436
+ syncRetries?: number;
437
+ cacheCapacity?: number;
438
+ cacheTtl?: number;
439
+ onFetch: (context: C, signal: AbortSignal) => Promise<T[]>;
440
+ onSync: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
441
+ };
442
+
443
+ // Change type for sync operations
444
+ type Change<T> = {
445
+ id: string;
446
+ type: "create" | "update" | "delete";
447
+ data: T;
448
+ };
449
+
450
+ // Sync result
451
+ type SyncResult = {
452
+ id: string;
453
+ status: "success" | "error";
454
+ error?: string;
455
+ newId?: string; // For create operations: server-assigned ID to replace temp ID
456
+ };
457
+
458
+ // Item status
459
+ type ItemStatus = {
460
+ type: "create" | "update" | "delete";
461
+ status: "pending" | "syncing" | "success" | "error";
462
+ retries: number;
463
+ error?: string;
464
+ } | null;
465
+
466
+ // Sync queue state
467
+ type SyncQueueState<T> = {
468
+ queue: Map<string, Change<T>>; // Pending changes
469
+ inFlight: Map<string, Change<T>>; // Currently syncing
470
+ errors: Map<string, { error: string; retries: number }>;
471
+ isPaused: boolean;
472
+ isSyncing: boolean;
473
+ };
474
+ ```
475
+
476
+ ## Best Practices
477
+
478
+ 1. **Use Optimistic Updates**: Let users see changes immediately while syncing in the background
479
+ 2. **Handle Errors Gracefully**: Show error states and provide retry mechanisms
480
+ 3. **Configure Debouncing**: Adjust `syncDebounce` based on your use case
481
+ 4. **Leverage Context**: Use context for filters, pagination, and search
482
+ 5. **Monitor Sync Queue**: Display pending changes and errors to users
483
+ 6. **Cache Wisely**: Configure `cacheTtl` and `cacheCapacity` based on your data freshness requirements
484
+
485
+ ## Architecture
486
+
487
+ The library is built on several core concepts:
488
+
489
+ - **Collection**: Manages the item collection, sync queue, and fetch handler
490
+ - **SyncQueue**: Handles debounced synchronization with retry logic
491
+ - **FetchHandler**: Manages data fetching with caching
492
+ - **Item**: Represents individual items with their sync state
493
+
494
+ All state updates use [Mutative](https://github.com/unadlib/mutative) for immutable updates, ensuring React can efficiently detect changes.
495
+
496
+ ## Contributing
497
+
498
+ This is an alpha release. Please read the source code for a deeper understanding of its implementation and capabilities. Contributions and feedback are welcome!
125
499
 
126
500
  ## License
127
501
 
128
- MIT
502
+ MIT
package/dist/App.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ declare function App(): import("react/jsx-runtime").JSX.Element;
2
+ export default App;
@@ -0,0 +1,12 @@
1
+ export declare class Cache<T> {
2
+ private _cache;
3
+ private _capacity;
4
+ private _ttl;
5
+ constructor(capacity: number, ttl: number);
6
+ get(key: string): T | null;
7
+ set(key: string, value: T): void;
8
+ invalidate(key: string): void;
9
+ clear(): void;
10
+ has(key: string): boolean;
11
+ get size(): number;
12
+ }
@@ -0,0 +1 @@
1
+ export {};