use-abcd 1.4.0 → 1.4.2
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 +217 -232
- package/dist/collection.d.ts +12 -3
- package/dist/examples/TreeEditor.d.ts +2 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1624 -968
- 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/node.d.ts +50 -0
- package/dist/node.test.d.ts +1 -0
- package/dist/sync-queue.d.ts +36 -0
- package/dist/types.d.ts +4 -1
- package/dist/useCrud.d.ts +1 -1
- package/dist/useCrud.test.d.ts +1 -0
- package/dist/useCrudTree.d.ts +45 -0
- package/dist/useCrudTree.test.d.ts +1 -0
- package/dist/useItem.d.ts +1 -1
- package/dist/useNode.d.ts +25 -0
- package/dist/useSelectedNode.d.ts +12 -0
- package/dist/useSyncState.d.ts +20 -0
- package/package.json +2 -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 are just CRUD operations in disguise. Stop fighting complex state management frameworks and reinventing patterns. This library gives you optimized CRUD state management with built-in sync and offline-first support. Get zero-latency updates, no loading screens, and effortless API integration. Your code stays simple, your users get instant responses.
|
|
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,22 +91,23 @@ 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> = {
|
|
92
|
-
id: string;
|
|
93
|
-
initialContext: C;
|
|
94
|
-
getId: (item: T) => string;
|
|
94
|
+
type Config<T extends object, C> = {
|
|
95
|
+
id: string; // Unique identifier for this collection
|
|
96
|
+
initialContext: C; // Initial context (filters, pagination, etc.)
|
|
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
|
-
syncDebounce?: number;
|
|
98
|
-
syncRetries?: number;
|
|
101
|
+
syncDebounce?: number; // Debounce delay for sync (default: 300ms)
|
|
102
|
+
syncRetries?: number; // Max retry attempts (default: 3)
|
|
99
103
|
|
|
100
104
|
// Optional cache configuration
|
|
101
|
-
cacheCapacity?: number;
|
|
102
|
-
cacheTtl?: number;
|
|
105
|
+
cacheCapacity?: number; // Max cache entries (default: 10)
|
|
106
|
+
cacheTtl?: number; // Cache TTL in ms (default: 60000)
|
|
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,64 @@ 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
|
-
|
|
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
|
+
}
|
|
211
194
|
```
|
|
212
195
|
|
|
213
|
-
**
|
|
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
|
-
```
|
|
196
|
+
**Benefits:**
|
|
245
197
|
|
|
246
|
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
249
|
-
- Pause/resume synchronization
|
|
250
|
-
- Per-item sync status
|
|
251
|
-
- Manual retry for errors
|
|
198
|
+
- `useItem()` subscribes only to that specific item's changes
|
|
199
|
+
- React re-renders only when that item's data changes (automatic optimization via WeakMap cache)
|
|
200
|
+
- Clean separation of list and item concerns
|
|
252
201
|
|
|
253
|
-
###
|
|
202
|
+
### Context-Based Filtering & Pagination
|
|
254
203
|
|
|
255
|
-
|
|
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.:
|
|
204
|
+
Use context to manage filters, pagination, sorting:
|
|
264
205
|
|
|
265
206
|
```typescript
|
|
266
207
|
interface ProductContext {
|
|
@@ -268,228 +209,272 @@ interface ProductContext {
|
|
|
268
209
|
limit: number;
|
|
269
210
|
category?: string;
|
|
270
211
|
search?: string;
|
|
271
|
-
sortBy?: "name" | "price";
|
|
272
212
|
}
|
|
273
213
|
|
|
274
|
-
const { context, setContext } = useCrud<Product, ProductContext>(config);
|
|
214
|
+
const { items, context, setContext } = useCrud<Product, ProductContext>(config);
|
|
275
215
|
|
|
276
|
-
// Update
|
|
216
|
+
// Update context to refetch
|
|
277
217
|
setContext((draft) => {
|
|
278
218
|
draft.category = "electronics";
|
|
279
219
|
draft.page = 1;
|
|
280
220
|
});
|
|
281
221
|
```
|
|
282
222
|
|
|
283
|
-
###
|
|
223
|
+
### Sync Queue Monitoring
|
|
284
224
|
|
|
285
225
|
Track pending changes and errors:
|
|
286
226
|
|
|
287
227
|
```typescript
|
|
288
228
|
const { syncQueue, pauseSync, resumeSync, retrySync } = useCrud(config);
|
|
289
229
|
|
|
230
|
+
// Check queue state
|
|
290
231
|
console.log({
|
|
291
232
|
pending: syncQueue.queue.size,
|
|
292
233
|
inFlight: syncQueue.inFlight.size,
|
|
293
234
|
errors: syncQueue.errors.size,
|
|
294
|
-
isPaused: syncQueue.isPaused,
|
|
295
|
-
isSyncing: syncQueue.isSyncing,
|
|
296
235
|
});
|
|
297
236
|
|
|
298
|
-
//
|
|
237
|
+
// Control sync
|
|
299
238
|
pauseSync();
|
|
300
|
-
|
|
301
|
-
// Resume sync
|
|
302
239
|
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
|
-
}
|
|
240
|
+
retrySync(); // Retry all failed items
|
|
241
|
+
retrySync(itemId); // Retry specific item
|
|
327
242
|
```
|
|
328
243
|
|
|
329
244
|
### ID Remapping for Optimistic Creates
|
|
330
245
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
**In your `onSync` handler**, return the server-assigned `newId` for create operations:
|
|
246
|
+
Handle temporary IDs that get replaced by server-assigned IDs:
|
|
334
247
|
|
|
335
248
|
```typescript
|
|
336
249
|
onSync: async (changes, signal) => {
|
|
337
|
-
const results: SyncResult[] = [];
|
|
338
|
-
|
|
339
250
|
for (const change of changes) {
|
|
340
251
|
if (change.type === "create") {
|
|
341
252
|
const response = await fetch("/api/items", {
|
|
342
253
|
method: "POST",
|
|
343
|
-
headers: { "Content-Type": "application/json" },
|
|
344
254
|
body: JSON.stringify(change.data),
|
|
345
255
|
signal,
|
|
346
256
|
});
|
|
347
|
-
|
|
348
|
-
if (!response.ok) throw new Error("Failed to create");
|
|
349
|
-
|
|
350
257
|
const data = await response.json();
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
258
|
+
|
|
259
|
+
// Return newId to remap temp ID to server ID
|
|
260
|
+
return {
|
|
261
|
+
id: change.id, // Temporary ID (e.g., "temp-123")
|
|
354
262
|
status: "success",
|
|
355
|
-
newId: data.id,
|
|
356
|
-
}
|
|
263
|
+
newId: data.id, // Server-assigned ID (e.g., "456")
|
|
264
|
+
};
|
|
357
265
|
}
|
|
358
266
|
// ... handle update and delete
|
|
359
267
|
}
|
|
360
|
-
|
|
361
|
-
return results;
|
|
362
268
|
};
|
|
363
269
|
```
|
|
364
270
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
271
|
+
The library automatically:
|
|
272
|
+
|
|
273
|
+
1. Updates the item's key in the `items` Map
|
|
274
|
+
2. Updates the item's `id` property
|
|
275
|
+
3. Updates any `Item` references
|
|
276
|
+
4. Triggers UI re-render
|
|
370
277
|
|
|
371
|
-
|
|
278
|
+
## End-to-End Type-Safe CRUD with createSyncClient & createSyncServer
|
|
279
|
+
|
|
280
|
+
Build a complete type-safe CRUD solution with minimal boilerplate:
|
|
281
|
+
|
|
282
|
+
### Client Setup
|
|
372
283
|
|
|
373
284
|
```typescript
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
285
|
+
import { useCrud, createSyncClientFromEndpoint } from "use-abcd";
|
|
286
|
+
|
|
287
|
+
interface User {
|
|
288
|
+
id: string;
|
|
289
|
+
name: string;
|
|
290
|
+
email: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
interface UserQuery {
|
|
294
|
+
page: number;
|
|
295
|
+
limit: number;
|
|
296
|
+
search?: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const UserConfig: Config<User, UserQuery> = {
|
|
300
|
+
id: "users",
|
|
301
|
+
initialContext: { page: 1, limit: 10 },
|
|
302
|
+
getId: (user) => user.id,
|
|
303
|
+
|
|
304
|
+
// Use createSyncClientFromEndpoint for unified fetch + sync
|
|
305
|
+
...createSyncClientFromEndpoint<User, UserQuery>("/api/users"),
|
|
378
306
|
};
|
|
379
|
-
```
|
|
380
307
|
|
|
381
|
-
|
|
308
|
+
function UserList() {
|
|
309
|
+
const { items, loading, create, update, remove, setContext } = useCrud(UserConfig);
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<div>
|
|
313
|
+
{loading ? <p>Loading...</p> : (
|
|
314
|
+
<>
|
|
315
|
+
{Array.from(items.values()).map((user) => (
|
|
316
|
+
<div key={user.id}>
|
|
317
|
+
<span>{user.name} - {user.email}</span>
|
|
318
|
+
<button onClick={() => update(user.id, (draft) => {
|
|
319
|
+
draft.name = "Updated Name";
|
|
320
|
+
})}>
|
|
321
|
+
Update
|
|
322
|
+
</button>
|
|
323
|
+
<button onClick={() => remove(user.id)}>Delete</button>
|
|
324
|
+
</div>
|
|
325
|
+
))}
|
|
326
|
+
<button onClick={() => create({
|
|
327
|
+
id: `temp-${Date.now()}`,
|
|
328
|
+
name: "New User",
|
|
329
|
+
email: "new@example.com",
|
|
330
|
+
})}>
|
|
331
|
+
Add User
|
|
332
|
+
</button>
|
|
333
|
+
</>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
```
|
|
382
339
|
|
|
383
|
-
|
|
340
|
+
### Server Setup
|
|
384
341
|
|
|
385
342
|
```typescript
|
|
386
|
-
|
|
387
|
-
// ...
|
|
388
|
-
cacheCapacity: 20, // Store up to 20 cache entries
|
|
389
|
-
cacheTtl: 30000, // Cache expires after 30 seconds
|
|
390
|
-
};
|
|
343
|
+
import { createSyncServer, serverSyncSuccess } from "use-abcd/runtime/server";
|
|
391
344
|
|
|
392
|
-
|
|
345
|
+
// Define your handlers
|
|
346
|
+
const usersHandler = createSyncServer<User, UserQuery>({
|
|
347
|
+
fetch: async (query) => {
|
|
348
|
+
// Handle pagination and search
|
|
349
|
+
return db.users.findMany({
|
|
350
|
+
skip: (query.page - 1) * query.limit,
|
|
351
|
+
take: query.limit,
|
|
352
|
+
where: query.search ? { name: { contains: query.search } } : undefined,
|
|
353
|
+
});
|
|
354
|
+
},
|
|
393
355
|
|
|
394
|
-
|
|
395
|
-
await
|
|
396
|
-
|
|
356
|
+
create: async (data) => {
|
|
357
|
+
const user = await db.users.create({ data });
|
|
358
|
+
return serverSyncSuccess({ newId: user.id });
|
|
359
|
+
},
|
|
397
360
|
|
|
398
|
-
|
|
361
|
+
update: async (id, data) => {
|
|
362
|
+
await db.users.update({ where: { id }, data });
|
|
363
|
+
return serverSyncSuccess();
|
|
364
|
+
},
|
|
399
365
|
|
|
400
|
-
|
|
366
|
+
delete: async (id) => {
|
|
367
|
+
await db.users.delete({ where: { id } });
|
|
368
|
+
return serverSyncSuccess();
|
|
369
|
+
},
|
|
370
|
+
});
|
|
401
371
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
cd use-abcd
|
|
372
|
+
// Use with your framework
|
|
373
|
+
// Next.js App Router
|
|
374
|
+
export const POST = usersHandler.handler;
|
|
406
375
|
|
|
407
|
-
|
|
408
|
-
|
|
376
|
+
// Hono
|
|
377
|
+
app.post("/api/users", (c) => usersHandler.handler(c.req.raw));
|
|
409
378
|
|
|
410
|
-
|
|
411
|
-
|
|
379
|
+
// Bun.serve
|
|
380
|
+
Bun.serve({
|
|
381
|
+
fetch(req) {
|
|
382
|
+
if (new URL(req.url).pathname === "/api/users") {
|
|
383
|
+
return usersHandler.handler(req);
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
});
|
|
412
387
|
```
|
|
413
388
|
|
|
414
|
-
|
|
389
|
+
### What You Get
|
|
415
390
|
|
|
416
|
-
|
|
391
|
+
- **Type safety**: Full TypeScript inference from data types to API calls
|
|
392
|
+
- **Automatic ID remapping**: Temporary IDs are replaced with server-assigned IDs
|
|
393
|
+
- **Batch operations**: Multiple changes are sent in a single request
|
|
394
|
+
- **Optimistic updates**: UI updates instantly, syncs in background
|
|
395
|
+
- **Error handling**: Failed operations are tracked and can be retried
|
|
396
|
+
- **Unified endpoint**: Single POST endpoint handles fetch + create/update/delete
|
|
417
397
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
398
|
+
### Request/Response Format
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// Fetch + Sync in one request
|
|
402
|
+
POST /api/users
|
|
403
|
+
Body: {
|
|
404
|
+
query: { page: 1, limit: 10, search: "john" },
|
|
405
|
+
changes: [
|
|
406
|
+
{ id: "temp-123", type: "create", data: { ... } },
|
|
407
|
+
{ id: "456", type: "update", data: { ... } },
|
|
408
|
+
{ id: "789", type: "delete", data: { ... } }
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
Response: {
|
|
413
|
+
results: [...users], // Fetched items
|
|
414
|
+
syncResults: [ // Sync results
|
|
415
|
+
{ id: "temp-123", status: "success", newId: "999" },
|
|
416
|
+
{ id: "456", status: "success" },
|
|
417
|
+
{ id: "789", status: "success" }
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
```
|
|
423
421
|
|
|
424
422
|
## API Reference
|
|
425
423
|
|
|
426
424
|
### Types
|
|
427
425
|
|
|
428
426
|
```typescript
|
|
429
|
-
|
|
430
|
-
type Config<T, C> = {
|
|
427
|
+
type Config<T extends object, C> = {
|
|
431
428
|
id: string;
|
|
432
429
|
initialContext: C;
|
|
433
430
|
getId: (item: T) => string;
|
|
434
|
-
setId?: (item: T, newId: string) => T;
|
|
431
|
+
setId?: (item: T, newId: string) => T;
|
|
435
432
|
syncDebounce?: number;
|
|
436
433
|
syncRetries?: number;
|
|
437
434
|
cacheCapacity?: number;
|
|
438
435
|
cacheTtl?: number;
|
|
439
436
|
onFetch: (context: C, signal: AbortSignal) => Promise<T[]>;
|
|
440
|
-
onSync
|
|
437
|
+
onSync?: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
|
|
441
438
|
};
|
|
442
439
|
|
|
443
|
-
// Change type for sync operations
|
|
444
440
|
type Change<T> = {
|
|
445
441
|
id: string;
|
|
446
442
|
type: "create" | "update" | "delete";
|
|
447
443
|
data: T;
|
|
448
444
|
};
|
|
449
445
|
|
|
450
|
-
// Sync result
|
|
451
446
|
type SyncResult = {
|
|
452
447
|
id: string;
|
|
453
448
|
status: "success" | "error";
|
|
454
449
|
error?: string;
|
|
455
|
-
newId?: string;
|
|
450
|
+
newId?: string; // For creates: server-assigned ID
|
|
456
451
|
};
|
|
457
452
|
|
|
458
|
-
// Item status
|
|
459
453
|
type ItemStatus = {
|
|
460
454
|
type: "create" | "update" | "delete";
|
|
461
455
|
status: "pending" | "syncing" | "success" | "error";
|
|
462
456
|
retries: number;
|
|
463
457
|
error?: string;
|
|
464
458
|
} | 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
459
|
```
|
|
475
460
|
|
|
476
461
|
## Best Practices
|
|
477
462
|
|
|
478
|
-
1. **Use Optimistic Updates
|
|
479
|
-
2. **Handle Errors Gracefully
|
|
480
|
-
3. **
|
|
481
|
-
4. **
|
|
482
|
-
5. **Monitor Sync Queue
|
|
483
|
-
6. **
|
|
463
|
+
1. **Use Optimistic Updates** - Let users see changes immediately while syncing in the background
|
|
464
|
+
2. **Handle Errors Gracefully** - Show error states and provide retry mechanisms
|
|
465
|
+
3. **Leverage Context** - Use context for filters, pagination, and search to trigger automatic refetches
|
|
466
|
+
4. **Use getItem() + useItem()** - For individual item management with automatic React optimization
|
|
467
|
+
5. **Monitor Sync Queue** - Display pending changes and errors to users for transparency
|
|
468
|
+
6. **Use createSyncClient/Server** - For end-to-end type-safe CRUD with minimal boilerplate
|
|
484
469
|
|
|
485
470
|
## Architecture
|
|
486
471
|
|
|
487
472
|
The library is built on several core concepts:
|
|
488
473
|
|
|
489
|
-
- **Collection
|
|
490
|
-
- **SyncQueue
|
|
491
|
-
- **FetchHandler
|
|
492
|
-
- **Item
|
|
474
|
+
- **Collection** - Manages the item collection, sync queue, and fetch handler
|
|
475
|
+
- **SyncQueue** - Handles debounced synchronization with retry logic
|
|
476
|
+
- **FetchHandler** - Manages data fetching with caching
|
|
477
|
+
- **Item** - Represents individual items with WeakMap-based caching for React optimization
|
|
493
478
|
|
|
494
479
|
All state updates use [Mutative](https://github.com/unadlib/mutative) for immutable updates, ensuring React can efficiently detect changes.
|
|
495
480
|
|
|
@@ -499,4 +484,4 @@ This is an alpha release. Please read the source code for a deeper understanding
|
|
|
499
484
|
|
|
500
485
|
## License
|
|
501
486
|
|
|
502
|
-
MIT
|
|
487
|
+
MIT
|
package/dist/collection.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Draft } from 'mutative';
|
|
2
2
|
import { Item } from './item';
|
|
3
|
+
import { Node } from './node';
|
|
3
4
|
import { Config, SyncState, Mutator, ItemStatus, SyncQueueState, FetchState } from './types';
|
|
4
5
|
export type CollectionState<T, C> = {
|
|
5
6
|
context: C;
|
|
@@ -11,17 +12,20 @@ export type CollectionState<T, C> = {
|
|
|
11
12
|
fetchStatus: FetchState;
|
|
12
13
|
fetchError?: string;
|
|
13
14
|
};
|
|
14
|
-
export declare class Collection<T, C> {
|
|
15
|
+
export declare class Collection<T extends object, C> {
|
|
15
16
|
private static _cache;
|
|
16
|
-
static get<T, C>(config: Config<T, C>): Collection<T, C>;
|
|
17
|
+
static get<T extends object, C>(config: Config<T, C>): Collection<T, C>;
|
|
17
18
|
static clear(id: string): void;
|
|
18
19
|
static clearAll(): void;
|
|
20
|
+
static getById<T extends object, C>(id: string): Collection<T, C> | undefined;
|
|
19
21
|
readonly id: string;
|
|
20
22
|
readonly config: Config<T, C>;
|
|
21
23
|
private _state;
|
|
22
24
|
private _syncQueue;
|
|
23
25
|
private _fetchHandler;
|
|
24
26
|
private _itemCache;
|
|
27
|
+
private _nodeCache;
|
|
28
|
+
private _selectedNodeId;
|
|
25
29
|
private _subscribers;
|
|
26
30
|
private _hasInitialized;
|
|
27
31
|
private _batchMode;
|
|
@@ -38,7 +42,12 @@ export declare class Collection<T, C> {
|
|
|
38
42
|
update(id: string, mutate: (draft: Draft<T>) => void): void;
|
|
39
43
|
remove(id: string): void;
|
|
40
44
|
getItem(id: string): Item<T, C>;
|
|
41
|
-
|
|
45
|
+
getNode<V extends object, NodeType = string>(id: string): Node<V, C, NodeType>;
|
|
46
|
+
batch(fn: () => void): void;
|
|
47
|
+
selectNode(id: string): void;
|
|
48
|
+
deselectNode(): void;
|
|
49
|
+
get selectedNodeId(): string | null;
|
|
50
|
+
get selectedNode(): Node<any, C> | null;
|
|
42
51
|
setContext(patchContext: Mutator<C>): void;
|
|
43
52
|
refresh(): Promise<void>;
|
|
44
53
|
pauseSync(): void;
|