use-abcd 1.4.0 → 1.4.1
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.
- package/README.md +209 -226
- package/dist/collection.d.ts +2 -3
- package/dist/index.js +203 -213
- package/dist/index.js.map +1 -1
- package/dist/item-cache.test.d.ts +1 -0
- package/dist/item.d.ts +1 -5
- package/dist/types.d.ts +1 -1
- package/dist/useCrud.d.ts +1 -1
- package/dist/useItem.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/smtrd3/common-state/actions)
|
|
4
4
|
|
|
5
|
+
Most apps on the internet are some form of CRUD - whether it's a todo list, a dashboard, or a social media feed. Yet we often find ourselves fighting with complex state management frameworks, reinventing patterns for each project. What if we inverted the problem? Instead of building custom state management, recognize that your app state is likely CRUD at its core. This library provides highly optimized, systematic CRUD state management that makes your code easier to reason about and your life as a developer much simpler. You don't need to invent state management for each app - chances are, it's just CRUD.
|
|
6
|
+
|
|
5
7
|
A powerful React hook for managing ABCD (or CRUD) operations with optimistic updates, caching, and automatic state management.
|
|
6
8
|
|
|
7
9
|
> **Note on Package Name**: The package is published as `use-abcd` on npm due to naming availability, where ABCD stands for Add, Browse, Change, and Delete - which maps directly to the traditional CRUD (Create, Read, Update, Delete) operations. While the package name uses ABCD, all internal APIs and documentation use CRUD terminology for familiarity and consistency with common programming patterns.
|
|
@@ -15,6 +17,7 @@ A powerful React hook for managing ABCD (or CRUD) operations with optimistic upd
|
|
|
15
17
|
- ⏳ Debounced sync with configurable delays
|
|
16
18
|
- 🔄 Sync queue management with pause/resume/retry
|
|
17
19
|
- 🎨 Context-based filtering and pagination
|
|
20
|
+
- 🔌 End-to-end type-safe client-server sync utilities
|
|
18
21
|
|
|
19
22
|
## Installation
|
|
20
23
|
|
|
@@ -88,10 +91,11 @@ function TodoList() {
|
|
|
88
91
|
The `Config` object defines how your data is fetched, synced, and managed:
|
|
89
92
|
|
|
90
93
|
```typescript
|
|
91
|
-
type Config<T, C> = {
|
|
94
|
+
type Config<T extends object, C> = {
|
|
92
95
|
id: string; // Unique identifier for this collection
|
|
93
96
|
initialContext: C; // Initial context (filters, pagination, etc.)
|
|
94
97
|
getId: (item: T) => string; // Extract ID from item
|
|
98
|
+
setId?: (item: T, newId: string) => T; // Optional: for ID remapping on create
|
|
95
99
|
|
|
96
100
|
// Optional sync configuration
|
|
97
101
|
syncDebounce?: number; // Debounce delay for sync (default: 300ms)
|
|
@@ -103,7 +107,7 @@ type Config<T, C> = {
|
|
|
103
107
|
|
|
104
108
|
// Required handlers
|
|
105
109
|
onFetch: (context: C, signal: AbortSignal) => Promise<T[]>;
|
|
106
|
-
onSync
|
|
110
|
+
onSync?: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
|
|
107
111
|
};
|
|
108
112
|
```
|
|
109
113
|
|
|
@@ -119,7 +123,6 @@ The `useCrud` hook returns:
|
|
|
119
123
|
loading: boolean; // Fetch loading state
|
|
120
124
|
syncing: boolean; // Sync in progress
|
|
121
125
|
syncQueue: SyncQueueState<T>; // Sync queue state
|
|
122
|
-
syncState: SyncState; // Overall sync state
|
|
123
126
|
|
|
124
127
|
// Item operations (optimistic)
|
|
125
128
|
create: (item: T) => void;
|
|
@@ -141,126 +144,63 @@ The `useCrud` hook returns:
|
|
|
141
144
|
|
|
142
145
|
## Examples
|
|
143
146
|
|
|
144
|
-
The repository includes
|
|
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,
|
|
160
|
-
|
|
161
|
-
onFetch: async (context, signal) => {
|
|
162
|
-
const params = new URLSearchParams({
|
|
163
|
-
page: String(context.page),
|
|
164
|
-
limit: String(context.limit),
|
|
165
|
-
});
|
|
166
|
-
if (context.category) params.append("category", context.category);
|
|
167
|
-
if (context.search) params.append("search", context.search);
|
|
147
|
+
The repository includes examples demonstrating various use cases:
|
|
168
148
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
149
|
+
- **Products** - Full CRUD with filtering, search, and error handling
|
|
150
|
+
- **Pagination** - Context-based pagination with dynamic page size
|
|
151
|
+
- **Optimistic Updates** - Comments with instant UI feedback and sync queue visualization
|
|
152
|
+
- **Blog Post** - Simple single-item editing
|
|
153
|
+
- **Todo List** - Basic list operations
|
|
172
154
|
|
|
173
|
-
|
|
174
|
-
// Handle batch sync operations
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
```
|
|
155
|
+
Run locally: `bun run dev` or `npm run dev` and visit `http://localhost:5173`
|
|
178
156
|
|
|
179
|
-
|
|
180
|
-
- Filtering by category
|
|
181
|
-
- Text search
|
|
182
|
-
- Pause/resume sync
|
|
183
|
-
- Retry failed operations
|
|
184
|
-
- Visual status indicators
|
|
157
|
+
## Advanced Usage
|
|
185
158
|
|
|
186
|
-
###
|
|
159
|
+
### Individual Item Management
|
|
187
160
|
|
|
188
|
-
|
|
189
|
-
- Dynamic page size selection
|
|
190
|
-
- Next/previous navigation
|
|
191
|
-
- Context updates trigger re-fetch
|
|
161
|
+
Use `getItem()` with `useItem()` for managing individual items:
|
|
192
162
|
|
|
193
163
|
```typescript
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
164
|
+
import { useCrud, useItem } from "use-abcd";
|
|
165
|
+
|
|
166
|
+
function ProductList() {
|
|
167
|
+
const { items, getItem } = useCrud(ProductsConfig);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div>
|
|
171
|
+
{Array.from(items.keys()).map((id) => (
|
|
172
|
+
<ProductItem key={id} item={getItem(id)} />
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
197
176
|
}
|
|
198
177
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
});
|
|
178
|
+
function ProductItem({ item }: { item: Item<Product, ProductContext> }) {
|
|
179
|
+
const { data, status, update, remove, exists } = useItem(item);
|
|
180
|
+
|
|
181
|
+
if (!exists) return null;
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div>
|
|
185
|
+
<h3>{data?.name}</h3>
|
|
186
|
+
<p>Status: {status?.status || "synced"}</p>
|
|
187
|
+
<button onClick={() => update((draft) => { draft.stock += 1; })}>
|
|
188
|
+
Add Stock
|
|
189
|
+
</button>
|
|
190
|
+
<button onClick={() => remove()}>Delete</button>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
244
194
|
```
|
|
245
195
|
|
|
246
|
-
**
|
|
247
|
-
-
|
|
248
|
-
-
|
|
249
|
-
-
|
|
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
|
|
196
|
+
**Benefits:**
|
|
197
|
+
- `useItem()` subscribes only to that specific item's changes
|
|
198
|
+
- React re-renders only when that item's data changes (automatic optimization via WeakMap cache)
|
|
199
|
+
- Clean separation of list and item concerns
|
|
258
200
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
### Custom Context for Filtering
|
|
201
|
+
### Context-Based Filtering & Pagination
|
|
262
202
|
|
|
263
|
-
Use context to manage filters, pagination, sorting
|
|
203
|
+
Use context to manage filters, pagination, sorting:
|
|
264
204
|
|
|
265
205
|
```typescript
|
|
266
206
|
interface ProductContext {
|
|
@@ -268,228 +208,271 @@ interface ProductContext {
|
|
|
268
208
|
limit: number;
|
|
269
209
|
category?: string;
|
|
270
210
|
search?: string;
|
|
271
|
-
sortBy?: "name" | "price";
|
|
272
211
|
}
|
|
273
212
|
|
|
274
|
-
const { context, setContext } = useCrud<Product, ProductContext>(config);
|
|
213
|
+
const { items, context, setContext } = useCrud<Product, ProductContext>(config);
|
|
275
214
|
|
|
276
|
-
// Update
|
|
215
|
+
// Update context to refetch
|
|
277
216
|
setContext((draft) => {
|
|
278
217
|
draft.category = "electronics";
|
|
279
218
|
draft.page = 1;
|
|
280
219
|
});
|
|
281
220
|
```
|
|
282
221
|
|
|
283
|
-
###
|
|
222
|
+
### Sync Queue Monitoring
|
|
284
223
|
|
|
285
224
|
Track pending changes and errors:
|
|
286
225
|
|
|
287
226
|
```typescript
|
|
288
227
|
const { syncQueue, pauseSync, resumeSync, retrySync } = useCrud(config);
|
|
289
228
|
|
|
229
|
+
// Check queue state
|
|
290
230
|
console.log({
|
|
291
231
|
pending: syncQueue.queue.size,
|
|
292
232
|
inFlight: syncQueue.inFlight.size,
|
|
293
233
|
errors: syncQueue.errors.size,
|
|
294
|
-
isPaused: syncQueue.isPaused,
|
|
295
|
-
isSyncing: syncQueue.isSyncing,
|
|
296
234
|
});
|
|
297
235
|
|
|
298
|
-
//
|
|
236
|
+
// Control sync
|
|
299
237
|
pauseSync();
|
|
300
|
-
|
|
301
|
-
// Resume sync
|
|
302
238
|
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
|
-
}
|
|
239
|
+
retrySync(); // Retry all failed items
|
|
240
|
+
retrySync(itemId); // Retry specific item
|
|
327
241
|
```
|
|
328
242
|
|
|
329
243
|
### ID Remapping for Optimistic Creates
|
|
330
244
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
**In your `onSync` handler**, return the server-assigned `newId` for create operations:
|
|
245
|
+
Handle temporary IDs that get replaced by server-assigned IDs:
|
|
334
246
|
|
|
335
247
|
```typescript
|
|
336
248
|
onSync: async (changes, signal) => {
|
|
337
|
-
const results: SyncResult[] = [];
|
|
338
|
-
|
|
339
249
|
for (const change of changes) {
|
|
340
250
|
if (change.type === "create") {
|
|
341
251
|
const response = await fetch("/api/items", {
|
|
342
252
|
method: "POST",
|
|
343
|
-
headers: { "Content-Type": "application/json" },
|
|
344
253
|
body: JSON.stringify(change.data),
|
|
345
254
|
signal,
|
|
346
255
|
});
|
|
347
|
-
|
|
348
|
-
if (!response.ok) throw new Error("Failed to create");
|
|
349
|
-
|
|
350
256
|
const data = await response.json();
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
257
|
+
|
|
258
|
+
// Return newId to remap temp ID to server ID
|
|
259
|
+
return {
|
|
260
|
+
id: change.id, // Temporary ID (e.g., "temp-123")
|
|
354
261
|
status: "success",
|
|
355
|
-
newId: data.id, //
|
|
356
|
-
}
|
|
262
|
+
newId: data.id, // Server-assigned ID (e.g., "456")
|
|
263
|
+
};
|
|
357
264
|
}
|
|
358
265
|
// ... handle update and delete
|
|
359
266
|
}
|
|
360
|
-
|
|
361
|
-
return results;
|
|
362
267
|
};
|
|
363
268
|
```
|
|
364
269
|
|
|
365
|
-
|
|
366
|
-
1.
|
|
367
|
-
2.
|
|
368
|
-
3.
|
|
369
|
-
4.
|
|
270
|
+
The library automatically:
|
|
271
|
+
1. Updates the item's key in the `items` Map
|
|
272
|
+
2. Updates the item's `id` property
|
|
273
|
+
3. Updates any `Item` references
|
|
274
|
+
4. Triggers UI re-render
|
|
370
275
|
|
|
371
|
-
|
|
276
|
+
## End-to-End Type-Safe CRUD with createSyncClient & createSyncServer
|
|
277
|
+
|
|
278
|
+
Build a complete type-safe CRUD solution with minimal boilerplate:
|
|
279
|
+
|
|
280
|
+
### Client Setup
|
|
372
281
|
|
|
373
282
|
```typescript
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
283
|
+
import { useCrud, createSyncClientFromEndpoint } from "use-abcd";
|
|
284
|
+
|
|
285
|
+
interface User {
|
|
286
|
+
id: string;
|
|
287
|
+
name: string;
|
|
288
|
+
email: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
interface UserQuery {
|
|
292
|
+
page: number;
|
|
293
|
+
limit: number;
|
|
294
|
+
search?: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const UserConfig: Config<User, UserQuery> = {
|
|
298
|
+
id: "users",
|
|
299
|
+
initialContext: { page: 1, limit: 10 },
|
|
300
|
+
getId: (user) => user.id,
|
|
301
|
+
|
|
302
|
+
// Use createSyncClientFromEndpoint for unified fetch + sync
|
|
303
|
+
...createSyncClientFromEndpoint<User, UserQuery>("/api/users"),
|
|
378
304
|
};
|
|
379
|
-
```
|
|
380
305
|
|
|
381
|
-
|
|
306
|
+
function UserList() {
|
|
307
|
+
const { items, loading, create, update, remove, setContext } = useCrud(UserConfig);
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<div>
|
|
311
|
+
{loading ? <p>Loading...</p> : (
|
|
312
|
+
<>
|
|
313
|
+
{Array.from(items.values()).map((user) => (
|
|
314
|
+
<div key={user.id}>
|
|
315
|
+
<span>{user.name} - {user.email}</span>
|
|
316
|
+
<button onClick={() => update(user.id, (draft) => {
|
|
317
|
+
draft.name = "Updated Name";
|
|
318
|
+
})}>
|
|
319
|
+
Update
|
|
320
|
+
</button>
|
|
321
|
+
<button onClick={() => remove(user.id)}>Delete</button>
|
|
322
|
+
</div>
|
|
323
|
+
))}
|
|
324
|
+
<button onClick={() => create({
|
|
325
|
+
id: `temp-${Date.now()}`,
|
|
326
|
+
name: "New User",
|
|
327
|
+
email: "new@example.com",
|
|
328
|
+
})}>
|
|
329
|
+
Add User
|
|
330
|
+
</button>
|
|
331
|
+
</>
|
|
332
|
+
)}
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
```
|
|
382
337
|
|
|
383
|
-
|
|
338
|
+
### Server Setup
|
|
384
339
|
|
|
385
340
|
```typescript
|
|
386
|
-
|
|
387
|
-
// ...
|
|
388
|
-
cacheCapacity: 20, // Store up to 20 cache entries
|
|
389
|
-
cacheTtl: 30000, // Cache expires after 30 seconds
|
|
390
|
-
};
|
|
341
|
+
import { createSyncServer, serverSyncSuccess } from "use-abcd/runtime/server";
|
|
391
342
|
|
|
392
|
-
|
|
343
|
+
// Define your handlers
|
|
344
|
+
const usersHandler = createSyncServer<User, UserQuery>({
|
|
345
|
+
fetch: async (query) => {
|
|
346
|
+
// Handle pagination and search
|
|
347
|
+
return db.users.findMany({
|
|
348
|
+
skip: (query.page - 1) * query.limit,
|
|
349
|
+
take: query.limit,
|
|
350
|
+
where: query.search ? { name: { contains: query.search } } : undefined,
|
|
351
|
+
});
|
|
352
|
+
},
|
|
393
353
|
|
|
394
|
-
|
|
395
|
-
await
|
|
396
|
-
|
|
354
|
+
create: async (data) => {
|
|
355
|
+
const user = await db.users.create({ data });
|
|
356
|
+
return serverSyncSuccess({ newId: user.id });
|
|
357
|
+
},
|
|
397
358
|
|
|
398
|
-
|
|
359
|
+
update: async (id, data) => {
|
|
360
|
+
await db.users.update({ where: { id }, data });
|
|
361
|
+
return serverSyncSuccess();
|
|
362
|
+
},
|
|
399
363
|
|
|
400
|
-
|
|
364
|
+
delete: async (id) => {
|
|
365
|
+
await db.users.delete({ where: { id } });
|
|
366
|
+
return serverSyncSuccess();
|
|
367
|
+
},
|
|
368
|
+
});
|
|
401
369
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
cd use-abcd
|
|
370
|
+
// Use with your framework
|
|
371
|
+
// Next.js App Router
|
|
372
|
+
export const POST = usersHandler.handler;
|
|
406
373
|
|
|
407
|
-
|
|
408
|
-
|
|
374
|
+
// Hono
|
|
375
|
+
app.post("/api/users", (c) => usersHandler.handler(c.req.raw));
|
|
409
376
|
|
|
410
|
-
|
|
411
|
-
|
|
377
|
+
// Bun.serve
|
|
378
|
+
Bun.serve({
|
|
379
|
+
fetch(req) {
|
|
380
|
+
if (new URL(req.url).pathname === "/api/users") {
|
|
381
|
+
return usersHandler.handler(req);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
412
385
|
```
|
|
413
386
|
|
|
414
|
-
|
|
387
|
+
### What You Get
|
|
388
|
+
|
|
389
|
+
- **Type safety**: Full TypeScript inference from data types to API calls
|
|
390
|
+
- **Automatic ID remapping**: Temporary IDs are replaced with server-assigned IDs
|
|
391
|
+
- **Batch operations**: Multiple changes are sent in a single request
|
|
392
|
+
- **Optimistic updates**: UI updates instantly, syncs in background
|
|
393
|
+
- **Error handling**: Failed operations are tracked and can be retried
|
|
394
|
+
- **Unified endpoint**: Single POST endpoint handles fetch + create/update/delete
|
|
415
395
|
|
|
416
|
-
###
|
|
396
|
+
### Request/Response Format
|
|
417
397
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
398
|
+
```typescript
|
|
399
|
+
// Fetch + Sync in one request
|
|
400
|
+
POST /api/users
|
|
401
|
+
Body: {
|
|
402
|
+
query: { page: 1, limit: 10, search: "john" },
|
|
403
|
+
changes: [
|
|
404
|
+
{ id: "temp-123", type: "create", data: { ... } },
|
|
405
|
+
{ id: "456", type: "update", data: { ... } },
|
|
406
|
+
{ id: "789", type: "delete", data: { ... } }
|
|
407
|
+
]
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
Response: {
|
|
411
|
+
results: [...users], // Fetched items
|
|
412
|
+
syncResults: [ // Sync results
|
|
413
|
+
{ id: "temp-123", status: "success", newId: "999" },
|
|
414
|
+
{ id: "456", status: "success" },
|
|
415
|
+
{ id: "789", status: "success" }
|
|
416
|
+
]
|
|
417
|
+
}
|
|
418
|
+
```
|
|
423
419
|
|
|
424
420
|
## API Reference
|
|
425
421
|
|
|
426
422
|
### Types
|
|
427
423
|
|
|
428
424
|
```typescript
|
|
429
|
-
|
|
430
|
-
type Config<T, C> = {
|
|
425
|
+
type Config<T extends object, C> = {
|
|
431
426
|
id: string;
|
|
432
427
|
initialContext: C;
|
|
433
428
|
getId: (item: T) => string;
|
|
434
|
-
setId?: (item: T, newId: string) => T;
|
|
429
|
+
setId?: (item: T, newId: string) => T;
|
|
435
430
|
syncDebounce?: number;
|
|
436
431
|
syncRetries?: number;
|
|
437
432
|
cacheCapacity?: number;
|
|
438
433
|
cacheTtl?: number;
|
|
439
434
|
onFetch: (context: C, signal: AbortSignal) => Promise<T[]>;
|
|
440
|
-
onSync
|
|
435
|
+
onSync?: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
|
|
441
436
|
};
|
|
442
437
|
|
|
443
|
-
// Change type for sync operations
|
|
444
438
|
type Change<T> = {
|
|
445
439
|
id: string;
|
|
446
440
|
type: "create" | "update" | "delete";
|
|
447
441
|
data: T;
|
|
448
442
|
};
|
|
449
443
|
|
|
450
|
-
// Sync result
|
|
451
444
|
type SyncResult = {
|
|
452
445
|
id: string;
|
|
453
446
|
status: "success" | "error";
|
|
454
447
|
error?: string;
|
|
455
|
-
newId?: string; // For
|
|
448
|
+
newId?: string; // For creates: server-assigned ID
|
|
456
449
|
};
|
|
457
450
|
|
|
458
|
-
// Item status
|
|
459
451
|
type ItemStatus = {
|
|
460
452
|
type: "create" | "update" | "delete";
|
|
461
453
|
status: "pending" | "syncing" | "success" | "error";
|
|
462
454
|
retries: number;
|
|
463
455
|
error?: string;
|
|
464
456
|
} | 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
457
|
```
|
|
475
458
|
|
|
476
459
|
## Best Practices
|
|
477
460
|
|
|
478
|
-
1. **Use Optimistic Updates
|
|
479
|
-
2. **Handle Errors Gracefully
|
|
480
|
-
3. **
|
|
481
|
-
4. **
|
|
482
|
-
5. **Monitor Sync Queue
|
|
483
|
-
6. **
|
|
461
|
+
1. **Use Optimistic Updates** - Let users see changes immediately while syncing in the background
|
|
462
|
+
2. **Handle Errors Gracefully** - Show error states and provide retry mechanisms
|
|
463
|
+
3. **Leverage Context** - Use context for filters, pagination, and search to trigger automatic refetches
|
|
464
|
+
4. **Use getItem() + useItem()** - For individual item management with automatic React optimization
|
|
465
|
+
5. **Monitor Sync Queue** - Display pending changes and errors to users for transparency
|
|
466
|
+
6. **Use createSyncClient/Server** - For end-to-end type-safe CRUD with minimal boilerplate
|
|
484
467
|
|
|
485
468
|
## Architecture
|
|
486
469
|
|
|
487
470
|
The library is built on several core concepts:
|
|
488
471
|
|
|
489
|
-
- **Collection
|
|
490
|
-
- **SyncQueue
|
|
491
|
-
- **FetchHandler
|
|
492
|
-
- **Item
|
|
472
|
+
- **Collection** - Manages the item collection, sync queue, and fetch handler
|
|
473
|
+
- **SyncQueue** - Handles debounced synchronization with retry logic
|
|
474
|
+
- **FetchHandler** - Manages data fetching with caching
|
|
475
|
+
- **Item** - Represents individual items with WeakMap-based caching for React optimization
|
|
493
476
|
|
|
494
477
|
All state updates use [Mutative](https://github.com/unadlib/mutative) for immutable updates, ensuring React can efficiently detect changes.
|
|
495
478
|
|
|
@@ -499,4 +482,4 @@ This is an alpha release. Please read the source code for a deeper understanding
|
|
|
499
482
|
|
|
500
483
|
## License
|
|
501
484
|
|
|
502
|
-
MIT
|
|
485
|
+
MIT
|
package/dist/collection.d.ts
CHANGED
|
@@ -11,9 +11,9 @@ export type CollectionState<T, C> = {
|
|
|
11
11
|
fetchStatus: FetchState;
|
|
12
12
|
fetchError?: string;
|
|
13
13
|
};
|
|
14
|
-
export declare class Collection<T, C> {
|
|
14
|
+
export declare class Collection<T extends object, C> {
|
|
15
15
|
private static _cache;
|
|
16
|
-
static get<T, C>(config: Config<T, C>): Collection<T, C>;
|
|
16
|
+
static get<T extends object, C>(config: Config<T, C>): Collection<T, C>;
|
|
17
17
|
static clear(id: string): void;
|
|
18
18
|
static clearAll(): void;
|
|
19
19
|
readonly id: string;
|
|
@@ -38,7 +38,6 @@ export declare class Collection<T, C> {
|
|
|
38
38
|
update(id: string, mutate: (draft: Draft<T>) => void): void;
|
|
39
39
|
remove(id: string): void;
|
|
40
40
|
getItem(id: string): Item<T, C>;
|
|
41
|
-
_releaseItem(id: string): void;
|
|
42
41
|
setContext(patchContext: Mutator<C>): void;
|
|
43
42
|
refresh(): Promise<void>;
|
|
44
43
|
pauseSync(): void;
|