use-abcd 1.4.3 → 1.6.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.
- package/README.md +225 -406
- package/dist/chunks/client-lHRDCo64.js +30 -0
- package/dist/chunks/client-lHRDCo64.js.map +1 -0
- package/dist/chunks/utils-DL5mgkvm.js +828 -0
- package/dist/chunks/utils-DL5mgkvm.js.map +1 -0
- package/dist/collection.d.ts +13 -5
- package/dist/examples/Benchmark.d.ts +2 -0
- package/dist/examples/LocalNotes.d.ts +2 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1071 -1789
- package/dist/index.js.map +1 -1
- package/dist/item.d.ts +3 -2
- package/dist/mocks/handlers.d.ts +0 -1
- package/dist/node.d.ts +5 -1
- package/dist/runtime/client.d.ts +10 -48
- package/dist/runtime/client.js +5 -13
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/index.d.ts +5 -5
- package/dist/runtime/local.d.ts +32 -0
- package/dist/runtime/server.d.ts +25 -35
- package/dist/runtime/server.js +59 -108
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/types.d.ts +28 -63
- package/dist/sync-queue.d.ts +7 -44
- package/dist/types.d.ts +22 -18
- package/dist/useCrud.d.ts +5 -3
- package/dist/useCrudTree.d.ts +3 -3
- package/dist/useItem.d.ts +3 -1
- package/dist/useNode.d.ts +3 -0
- package/dist/useSyncState.d.ts +2 -2
- package/dist/utils.d.ts +3 -4
- package/package.json +4 -2
- package/dist/cache.test.d.ts +0 -1
- package/dist/chunks/client-BfugfaiH.js +0 -144
- package/dist/chunks/client-BfugfaiH.js.map +0 -1
- package/dist/chunks/types-Dy4rYb2N.js +0 -19
- package/dist/chunks/types-Dy4rYb2N.js.map +0 -1
- package/dist/collection.e2e.test.d.ts +0 -1
- package/dist/fetch-handler.test.d.ts +0 -1
- package/dist/item-cache.test.d.ts +0 -1
- package/dist/node.test.d.ts +0 -1
- package/dist/runtime/client.test.d.ts +0 -1
- package/dist/runtime/server.test.d.ts +0 -1
- package/dist/sync-queue.test.d.ts +0 -1
- package/dist/useCrud.test.d.ts +0 -1
- package/dist/useCrudTree.test.d.ts +0 -1
- /package/dist/{App.d.ts → examples/App.d.ts} +0 -0
- /package/dist/{main.d.ts → examples/main.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -1,487 +1,306 @@
|
|
|
1
|
-
# use-abcd
|
|
1
|
+
# use-abcd
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
State management library purpose-built for CRUD applications. Manages collections of records with optimistic mutations, automatic syncing, and built-in retry logic. Includes a fullstack component for implementing state synchronization between client and server.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
A powerful React hook for managing ABCD (or CRUD) operations with optimistic updates, caching, and automatic state management.
|
|
8
|
-
|
|
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.
|
|
10
|
-
|
|
11
|
-
## Features
|
|
12
|
-
|
|
13
|
-
- 🔄 Automatic state management with React 19 compatible hooks
|
|
14
|
-
- ⚡ Optimistic updates for instant UI feedback
|
|
15
|
-
- 🗄️ Built-in caching with configurable TTL and capacity
|
|
16
|
-
- 🎯 Type-safe with full TypeScript support
|
|
17
|
-
- ⏳ Debounced sync with configurable delays
|
|
18
|
-
- 🔄 Sync queue management with pause/resume/retry
|
|
19
|
-
- 🎨 Context-based filtering and pagination
|
|
20
|
-
- 🔌 End-to-end type-safe client-server sync utilities
|
|
21
|
-
|
|
22
|
-
## Installation
|
|
23
|
-
|
|
24
|
-
```bash
|
|
5
|
+
```
|
|
25
6
|
npm install use-abcd
|
|
26
|
-
# or
|
|
27
|
-
yarn add use-abcd
|
|
28
|
-
# or
|
|
29
|
-
bun add use-abcd
|
|
30
7
|
```
|
|
31
8
|
|
|
32
|
-
##
|
|
33
|
-
|
|
34
|
-
The package provides multiple entry points:
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
// Main package - React hooks and client-side sync utilities
|
|
38
|
-
import { useCrud, Collection, createSyncClient } from "use-abcd";
|
|
39
|
-
|
|
40
|
-
// Runtime client - Client & server sync utilities (for isomorphic code)
|
|
41
|
-
import { createSyncClient, createSyncServer } from "use-abcd/runtime/client";
|
|
9
|
+
## Basic Usage
|
|
42
10
|
|
|
43
|
-
|
|
44
|
-
import { createSyncServer, serverSyncSuccess } from "use-abcd/runtime/server";
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Quick Start
|
|
11
|
+
A collection is defined by a `Config` object with an `id`, an `initialContext` (the query parameters), and a `handler` that fetches and syncs data.
|
|
48
12
|
|
|
49
|
-
```
|
|
50
|
-
import { useCrud,
|
|
13
|
+
```ts
|
|
14
|
+
import { useCrud, createSyncClient } from "use-abcd";
|
|
51
15
|
|
|
52
16
|
interface Todo {
|
|
53
17
|
id: string;
|
|
54
18
|
title: string;
|
|
55
|
-
|
|
19
|
+
done: boolean;
|
|
56
20
|
}
|
|
57
21
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
initialContext: {},
|
|
61
|
-
getId: (item) => item.id,
|
|
62
|
-
|
|
63
|
-
onFetch: async (context, signal) => {
|
|
64
|
-
const response = await fetch("/api/todos", { signal });
|
|
65
|
-
const data = await response.json();
|
|
66
|
-
return data.items;
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
onSync: async (changes, signal) => {
|
|
70
|
-
// Handle create, update, delete operations
|
|
71
|
-
const results = [];
|
|
72
|
-
for (const change of changes) {
|
|
73
|
-
// Process each change and return results
|
|
74
|
-
results.push({ id: change.id, status: "success" });
|
|
75
|
-
}
|
|
76
|
-
return results;
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
function TodoList() {
|
|
81
|
-
const { items, loading, create, update, remove } = useCrud(TodoConfig);
|
|
82
|
-
|
|
83
|
-
// Use items, create, update, remove in your UI
|
|
22
|
+
interface Query {
|
|
23
|
+
status: "all" | "active" | "done";
|
|
84
24
|
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Core Concepts
|
|
88
|
-
|
|
89
|
-
### Config
|
|
90
25
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
99
|
-
|
|
100
|
-
// Optional sync configuration
|
|
101
|
-
syncDebounce?: number; // Debounce delay for sync (default: 300ms)
|
|
102
|
-
syncRetries?: number; // Max retry attempts (default: 3)
|
|
103
|
-
|
|
104
|
-
// Optional cache configuration
|
|
105
|
-
cacheCapacity?: number; // Max cache entries (default: 10)
|
|
106
|
-
cacheTtl?: number; // Cache TTL in ms (default: 60000)
|
|
107
|
-
|
|
108
|
-
// Required handlers
|
|
109
|
-
onFetch: (context: C, signal: AbortSignal) => Promise<T[]>;
|
|
110
|
-
onSync?: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
|
|
26
|
+
const config = {
|
|
27
|
+
id: "todos",
|
|
28
|
+
initialContext: { status: "all" } as Query,
|
|
29
|
+
handler: createSyncClient<Todo, Query>("/api/todos"),
|
|
111
30
|
};
|
|
112
31
|
```
|
|
113
32
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
resumeSync: () => void;
|
|
141
|
-
retrySync: (id?: string) => void;
|
|
33
|
+
The `handler` is a single function that serves both fetching and syncing. When the collection needs data it calls the handler with `{ query }`. When local mutations need to be pushed it calls with `{ changes }`. `createSyncClient` creates a handler that talks to a remote endpoint over HTTP.
|
|
34
|
+
|
|
35
|
+
### `useCrud`
|
|
36
|
+
|
|
37
|
+
The main hook. Returns the collection's items, state flags, and mutation functions.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
function TodoApp() {
|
|
41
|
+
const {
|
|
42
|
+
items, // Map<string, Todo>
|
|
43
|
+
loading, // true during initial fetch
|
|
44
|
+
syncing, // true while pushing changes
|
|
45
|
+
context, // current query context
|
|
46
|
+
create, // (item: Omit<Todo, "id">) => string
|
|
47
|
+
update, // (id, (draft) => void) => void
|
|
48
|
+
remove, // (id) => void
|
|
49
|
+
setContext, // (mutator) => void — triggers refetch
|
|
50
|
+
getItem, // (id) => Item<Todo>
|
|
51
|
+
getItemStatus,// (id) => ItemStatus | null
|
|
52
|
+
refresh, // () => Promise<void>
|
|
53
|
+
pauseSync, // () => void
|
|
54
|
+
resumeSync, // () => void
|
|
55
|
+
retrySync, // (id?) => void
|
|
56
|
+
} = useCrud(config);
|
|
57
|
+
|
|
58
|
+
// ...render
|
|
142
59
|
}
|
|
143
60
|
```
|
|
144
61
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
The repository includes examples demonstrating various use cases:
|
|
148
|
-
|
|
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
|
|
62
|
+
Mutations are optimistic — `create`, `update`, and `remove` update local state immediately and queue changes for sync in the background. The sync queue batches changes, debounces flushes, and retries on failure.
|
|
154
63
|
|
|
155
|
-
|
|
64
|
+
### `useItem`
|
|
156
65
|
|
|
157
|
-
|
|
66
|
+
Subscribe to a single item without re-rendering on unrelated changes. Takes an `Item` reference from `getItem`.
|
|
158
67
|
|
|
159
|
-
|
|
68
|
+
```tsx
|
|
69
|
+
function TodoRow({ item }: { item: Item<Todo> }) {
|
|
70
|
+
const { data, status, update, remove, exists } = useItem(item);
|
|
160
71
|
|
|
161
|
-
|
|
72
|
+
// ...render
|
|
73
|
+
}
|
|
74
|
+
```
|
|
162
75
|
|
|
163
|
-
|
|
164
|
-
import { useCrud, useItem } from "use-abcd";
|
|
76
|
+
### Context
|
|
165
77
|
|
|
166
|
-
|
|
167
|
-
const { items, getItem } = useCrud(ProductsConfig);
|
|
78
|
+
Context drives the query sent to the handler on fetch. Changing it triggers a refetch.
|
|
168
79
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
</div>
|
|
175
|
-
);
|
|
176
|
-
}
|
|
80
|
+
```tsx
|
|
81
|
+
setContext((draft) => {
|
|
82
|
+
draft.status = "active";
|
|
83
|
+
});
|
|
84
|
+
```
|
|
177
85
|
|
|
178
|
-
|
|
179
|
-
const { data, status, update, remove, exists } = useItem(item);
|
|
86
|
+
### Config Options
|
|
180
87
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
88
|
+
```ts
|
|
89
|
+
{
|
|
90
|
+
id: string; // unique collection identifier
|
|
91
|
+
initialContext: C; // starting query state
|
|
92
|
+
handler?: CrudHandler; // fetch + sync function
|
|
93
|
+
serverItems?: T[]; // initial items for SSR hydration
|
|
94
|
+
|
|
95
|
+
// Sync
|
|
96
|
+
syncDebounce?: number; // ms, default 300
|
|
97
|
+
syncRetries?: number; // default 3
|
|
98
|
+
refetchOnMutation?: boolean; // refetch after create/delete, default false
|
|
99
|
+
|
|
100
|
+
// Cache
|
|
101
|
+
cacheCapacity?: number; // context cache slots, default 10
|
|
102
|
+
cacheTtl?: number; // ms, default 60000
|
|
103
|
+
|
|
104
|
+
// Fetch
|
|
105
|
+
fetchRetries?: number; // default 0
|
|
193
106
|
}
|
|
194
107
|
```
|
|
195
108
|
|
|
196
|
-
|
|
109
|
+
## Tree State
|
|
197
110
|
|
|
198
|
-
- `
|
|
199
|
-
- React re-renders only when that item's data changes (automatic optimization via WeakMap cache)
|
|
200
|
-
- Clean separation of list and item concerns
|
|
111
|
+
The library supports tree-shaped state using `useCrudTree`. Nodes are stored as a flat key-value map internally, with parent-child relationships encoded in the node IDs using a separator (default `.`). A node with id `root.settings.theme` is a child of `root.settings`.
|
|
201
112
|
|
|
202
|
-
|
|
113
|
+
```ts
|
|
114
|
+
import { useCrudTree, type TreeConfig } from "use-abcd";
|
|
203
115
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
interface ProductContext {
|
|
208
|
-
page: number;
|
|
209
|
-
limit: number;
|
|
210
|
-
category?: string;
|
|
211
|
-
search?: string;
|
|
116
|
+
interface FieldValue {
|
|
117
|
+
label: string;
|
|
212
118
|
}
|
|
213
119
|
|
|
214
|
-
|
|
120
|
+
type NodeType = "object" | "array" | "primitive";
|
|
215
121
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
122
|
+
const config: TreeConfig<FieldValue, {}, NodeType> = {
|
|
123
|
+
id: "tree-editor",
|
|
124
|
+
initialContext: {},
|
|
125
|
+
rootId: "root",
|
|
126
|
+
// nodeSeparator: ".", // default
|
|
127
|
+
handler: createSyncClient("/api/tree"),
|
|
128
|
+
};
|
|
221
129
|
```
|
|
222
130
|
|
|
223
|
-
###
|
|
131
|
+
### `useCrudTree`
|
|
224
132
|
|
|
225
|
-
|
|
133
|
+
Returns the root node, selection state, serialization, and all the standard sync controls.
|
|
226
134
|
|
|
227
|
-
```
|
|
228
|
-
|
|
135
|
+
```tsx
|
|
136
|
+
function TreeEditor() {
|
|
137
|
+
const {
|
|
138
|
+
rootNode, // Node | null
|
|
139
|
+
selectedNode, // Node | null
|
|
140
|
+
selectedNodeId, // string | null
|
|
141
|
+
selectNode, // (id) => void
|
|
142
|
+
deselectNode, // () => void
|
|
143
|
+
getNode, // (id) => Node
|
|
144
|
+
toJson, // () => object | null
|
|
145
|
+
// ...same sync controls as useCrud
|
|
146
|
+
} = useCrudTree(config);
|
|
229
147
|
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
pending: syncQueue.queue.size,
|
|
233
|
-
inFlight: syncQueue.inFlight.size,
|
|
234
|
-
errors: syncQueue.errors.size,
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// Control sync
|
|
238
|
-
pauseSync();
|
|
239
|
-
resumeSync();
|
|
240
|
-
retrySync(); // Retry all failed items
|
|
241
|
-
retrySync(itemId); // Retry specific item
|
|
148
|
+
// ...render
|
|
149
|
+
}
|
|
242
150
|
```
|
|
243
151
|
|
|
244
|
-
###
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
152
|
+
### `useNode`
|
|
153
|
+
|
|
154
|
+
Subscribe to a single tree node. Provides navigation, mutation, and reordering operations.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
function TreeNodeRow({ node }: { node: Node<FieldValue, {}, NodeType> }) {
|
|
158
|
+
const {
|
|
159
|
+
data, // TreeNode<FieldValue, NodeType> | undefined
|
|
160
|
+
children, // Node[]
|
|
161
|
+
depth, // nesting level
|
|
162
|
+
exists, // boolean
|
|
163
|
+
isSelected, // boolean
|
|
164
|
+
status, // ItemStatus
|
|
165
|
+
|
|
166
|
+
// Tree mutations
|
|
167
|
+
append, // (value, type?) => string — add child at end
|
|
168
|
+
prepend, // (value, type?) => string — add child at start
|
|
169
|
+
moveUp, // () => void
|
|
170
|
+
moveDown, // () => void
|
|
171
|
+
move, // (position, targetParent?) => void — reparent
|
|
172
|
+
clone, // () => Map — deep clone subtree
|
|
173
|
+
|
|
174
|
+
// Node mutations
|
|
175
|
+
updateProp, // (draft => void) => void — update value
|
|
176
|
+
remove, // () => void — remove node and descendants
|
|
177
|
+
|
|
178
|
+
// Selection
|
|
179
|
+
select, // () => void
|
|
180
|
+
deselect, // () => void
|
|
181
|
+
|
|
182
|
+
// Navigation
|
|
183
|
+
getParent, // () => Node | null
|
|
184
|
+
} = useNode(node);
|
|
185
|
+
|
|
186
|
+
// ...render
|
|
187
|
+
}
|
|
269
188
|
```
|
|
270
189
|
|
|
271
|
-
|
|
190
|
+
Each `TreeNode` stored in the collection has this shape:
|
|
272
191
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
192
|
+
```ts
|
|
193
|
+
{
|
|
194
|
+
id: string; // e.g. "root.settings.theme"
|
|
195
|
+
position: number; // sort order among siblings
|
|
196
|
+
value: T; // the node's data
|
|
197
|
+
type: NodeType; // e.g. "object" | "array" | "primitive"
|
|
198
|
+
}
|
|
199
|
+
```
|
|
279
200
|
|
|
280
|
-
|
|
201
|
+
### `useSelectedNode`
|
|
281
202
|
|
|
282
|
-
|
|
203
|
+
Convenience hook to access the currently selected node from anywhere, by collection ID.
|
|
283
204
|
|
|
284
|
-
```
|
|
285
|
-
|
|
205
|
+
```tsx
|
|
206
|
+
function Inspector() {
|
|
207
|
+
const { data, isPresent } = useSelectedNode<FieldValue, {}, NodeType>("tree-editor");
|
|
286
208
|
|
|
287
|
-
|
|
288
|
-
id: string;
|
|
289
|
-
name: string;
|
|
290
|
-
email: string;
|
|
209
|
+
// ...render
|
|
291
210
|
}
|
|
211
|
+
```
|
|
292
212
|
|
|
293
|
-
|
|
294
|
-
page: number;
|
|
295
|
-
limit: number;
|
|
296
|
-
search?: string;
|
|
297
|
-
}
|
|
213
|
+
## Server Contract
|
|
298
214
|
|
|
299
|
-
|
|
300
|
-
id: "users",
|
|
301
|
-
initialContext: { page: 1, limit: 10 },
|
|
302
|
-
getId: (user) => user.id,
|
|
215
|
+
The library communicates with the server through a single `POST` endpoint. Every request and response follows a fixed shape.
|
|
303
216
|
|
|
304
|
-
|
|
305
|
-
...createSyncClientFromEndpoint<User, UserQuery>("/api/users"),
|
|
306
|
-
};
|
|
217
|
+
### Request body
|
|
307
218
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
);
|
|
219
|
+
```ts
|
|
220
|
+
{
|
|
221
|
+
scope?: string; // optional namespace
|
|
222
|
+
query?: Q; // present on fetch requests
|
|
223
|
+
changes?: Change<T>[]; // present on sync requests
|
|
337
224
|
}
|
|
338
225
|
```
|
|
339
226
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
import { createSyncServer, serverSyncSuccess } from "use-abcd/runtime/server";
|
|
344
|
-
|
|
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
|
-
},
|
|
355
|
-
|
|
356
|
-
create: async (data) => {
|
|
357
|
-
const user = await db.users.create({ data });
|
|
358
|
-
return serverSyncSuccess({ newId: user.id });
|
|
359
|
-
},
|
|
360
|
-
|
|
361
|
-
update: async (id, data) => {
|
|
362
|
-
await db.users.update({ where: { id }, data });
|
|
363
|
-
return serverSyncSuccess();
|
|
364
|
-
},
|
|
365
|
-
|
|
366
|
-
delete: async (id) => {
|
|
367
|
-
await db.users.delete({ where: { id } });
|
|
368
|
-
return serverSyncSuccess();
|
|
369
|
-
},
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
// Use with your framework
|
|
373
|
-
// Next.js App Router
|
|
374
|
-
export const POST = usersHandler.handler;
|
|
375
|
-
|
|
376
|
-
// Hono
|
|
377
|
-
app.post("/api/users", (c) => usersHandler.handler(c.req.raw));
|
|
227
|
+
A request contains `query` (to fetch data), `changes` (to push mutations), or both.
|
|
378
228
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (new URL(req.url).pathname === "/api/users") {
|
|
383
|
-
return usersHandler.handler(req);
|
|
384
|
-
}
|
|
385
|
-
},
|
|
386
|
-
});
|
|
229
|
+
Each change:
|
|
230
|
+
```ts
|
|
231
|
+
{ id: string; type: "create" | "update" | "delete"; data: T }
|
|
387
232
|
```
|
|
388
233
|
|
|
389
|
-
###
|
|
390
|
-
|
|
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
|
|
397
|
-
|
|
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
|
-
}
|
|
234
|
+
### Response body
|
|
411
235
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
]
|
|
236
|
+
```ts
|
|
237
|
+
{
|
|
238
|
+
serverSyncedAt: string; // required — server timestamp (ULID)
|
|
239
|
+
items?: T[]; // returned items from a fetch
|
|
240
|
+
syncResults?: Result[]; // per-change results from a sync
|
|
241
|
+
serverState?: S; // optional server-side metadata
|
|
419
242
|
}
|
|
420
243
|
```
|
|
421
244
|
|
|
422
|
-
|
|
245
|
+
Each result:
|
|
246
|
+
```ts
|
|
247
|
+
{ id: string; type: ChangeType; status: "success" | "error"; serverSyncedAt: string; error?: string }
|
|
248
|
+
```
|
|
423
249
|
|
|
424
|
-
###
|
|
250
|
+
### Using the built-in runtime
|
|
425
251
|
|
|
426
|
-
|
|
427
|
-
type Config<T extends object, C> = {
|
|
428
|
-
id: string;
|
|
429
|
-
initialContext: C;
|
|
430
|
-
getId: (item: T) => string;
|
|
431
|
-
setId?: (item: T, newId: string) => T;
|
|
432
|
-
syncDebounce?: number;
|
|
433
|
-
syncRetries?: number;
|
|
434
|
-
cacheCapacity?: number;
|
|
435
|
-
cacheTtl?: number;
|
|
436
|
-
onFetch: (context: C, signal: AbortSignal) => Promise<T[]>;
|
|
437
|
-
onSync?: (changes: Change<T>[], signal: AbortSignal) => Promise<SyncResult[]>;
|
|
438
|
-
};
|
|
252
|
+
The library ships client and server helpers that implement this contract.
|
|
439
253
|
|
|
440
|
-
|
|
441
|
-
id: string;
|
|
442
|
-
type: "create" | "update" | "delete";
|
|
443
|
-
data: T;
|
|
444
|
-
};
|
|
254
|
+
**Client** — creates a `handler` for your config:
|
|
445
255
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
status: "success" | "error";
|
|
449
|
-
error?: string;
|
|
450
|
-
newId?: string; // For creates: server-assigned ID
|
|
451
|
-
};
|
|
256
|
+
```ts
|
|
257
|
+
import { createSyncClient } from "use-abcd";
|
|
452
258
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
259
|
+
const handler = createSyncClient<Todo, Query>("/api/todos");
|
|
260
|
+
// or with options:
|
|
261
|
+
const handler = createSyncClient<Todo, Query>({
|
|
262
|
+
endpoint: "/api/todos",
|
|
263
|
+
headers: { Authorization: "Bearer ..." },
|
|
264
|
+
scope: "workspace-123",
|
|
265
|
+
});
|
|
459
266
|
```
|
|
460
267
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
268
|
+
**Server** — creates a request handler from CRUD callbacks:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { createCrudHandler, createSyncServer } from "use-abcd/runtime/server";
|
|
272
|
+
|
|
273
|
+
const handler = createSyncServer(
|
|
274
|
+
createCrudHandler<Todo, Query>({
|
|
275
|
+
fetch: ({ scope, query }) => {
|
|
276
|
+
return db.todos.findMany({ where: { status: query.status } });
|
|
277
|
+
},
|
|
278
|
+
create: (record) => {
|
|
279
|
+
db.todos.insert(record.data);
|
|
280
|
+
},
|
|
281
|
+
update: (record) => {
|
|
282
|
+
db.todos.update(record.data.id, record.data);
|
|
283
|
+
},
|
|
284
|
+
remove: (record) => {
|
|
285
|
+
db.todos.delete(record.data.id);
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
```
|
|
473
290
|
|
|
474
|
-
|
|
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
|
|
291
|
+
`createSyncServer` returns a `(Request) => Promise<Response>` function compatible with any server that uses the Web Request/Response API (Bun, Deno, Cloudflare Workers, Next.js route handlers, etc.).
|
|
478
292
|
|
|
479
|
-
|
|
293
|
+
Each callback receives a `ServerRecord<T>`:
|
|
294
|
+
```ts
|
|
295
|
+
{ id: string; data: T; serverSyncedAt: string; deleted: boolean }
|
|
296
|
+
```
|
|
480
297
|
|
|
481
|
-
|
|
298
|
+
The `fetch` callback can return a plain array or an object with `items` and optional `serverState` for passing metadata (totals, pagination cursors, etc.) back to the client.
|
|
482
299
|
|
|
483
|
-
|
|
300
|
+
### Custom backend
|
|
484
301
|
|
|
485
|
-
|
|
302
|
+
If you are not using the built-in runtime, implement the POST endpoint yourself following the request/response shapes above. The key requirements:
|
|
486
303
|
|
|
487
|
-
|
|
304
|
+
1. Always return `serverSyncedAt` — a monotonically increasing string (ULIDs recommended). The client uses this for ordering and conflict detection.
|
|
305
|
+
2. Return `syncResults` for each change in the request. Each result must include the change's `id`, `type`, and a `status` of `"success"` or `"error"`. Missing results cause the sync queue to stall.
|
|
306
|
+
3. Return `items` when the request contains a `query`.
|