query-optimistic 0.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.
package/README.md ADDED
@@ -0,0 +1,371 @@
1
+ # query-optimistic
2
+
3
+ Simple, type-safe data fetching and optimistic updates for React.
4
+
5
+ A lightweight wrapper around TanStack Query that provides a cleaner API for defining queries, mutations, and handling optimistic updates with full TypeScript inference.
6
+
7
+ ## Features
8
+
9
+ - **Type-safe definitions** - Define queries and mutations once, get full type inference everywhere
10
+ - **Simplified optimistic updates** - Imperative channel API for intuitive UI updates
11
+ - **Automatic rollback** - Failed mutations automatically revert optimistic changes
12
+ - **Multiple query sync** - Update multiple queries/entities in a single mutation
13
+ - **Framework agnostic core** - Core utilities work without React
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install query-optimistic @tanstack/react-query
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import { defineCollection, defineMutation, useQuery, useMutation } from 'query-toolkit'
25
+
26
+ // Define a collection
27
+ const usersCollection = defineCollection({
28
+ name: 'users',
29
+ id: (user) => user.id,
30
+ fetch: () => api.get('/users')
31
+ })
32
+
33
+ // Define a mutation
34
+ const createUser = defineMutation({
35
+ mutate: (params: { name: string }) => api.post('/users', params)
36
+ })
37
+
38
+ // Use in components
39
+ function UserList() {
40
+ const [users, { isLoading }] = useQuery(usersCollection)
41
+
42
+ const { mutate } = useMutation(createUser, {
43
+ optimistic: (channel, params) => {
44
+ channel(usersCollection).prepend({
45
+ id: 'temp-' + Date.now(),
46
+ name: params.name,
47
+ }, { sync: true })
48
+ }
49
+ })
50
+
51
+ return (
52
+ <div>
53
+ <button onClick={() => mutate({ name: 'New User' })}>Add User</button>
54
+ {users?.map(user => <div key={user.id}>{user.name}</div>)}
55
+ </div>
56
+ )
57
+ }
58
+ ```
59
+
60
+ ## Defining Data Sources
61
+
62
+ ### Collections (Arrays)
63
+
64
+ ```typescript
65
+ interface User {
66
+ id: string
67
+ name: string
68
+ email: string
69
+ }
70
+
71
+ const usersCollection = defineCollection<User, { page?: number }>({
72
+ name: 'users',
73
+ id: (user) => user.id, // Required: how to identify items
74
+ fetch: ({ page = 1 }) => api.get(`/users?page=${page}`)
75
+ })
76
+ ```
77
+
78
+ ### Entities (Single Items)
79
+
80
+ ```typescript
81
+ interface Profile {
82
+ id: string
83
+ name: string
84
+ avatar: string
85
+ }
86
+
87
+ const profileEntity = defineEntity<Profile, string>({
88
+ name: 'profile',
89
+ fetch: (userId) => api.get(`/users/${userId}/profile`)
90
+ })
91
+ ```
92
+
93
+ ### Mutations
94
+
95
+ ```typescript
96
+ interface CreateUserParams {
97
+ name: string
98
+ email: string
99
+ }
100
+
101
+ const createUser = defineMutation<CreateUserParams, User>({
102
+ name: 'createUser', // Optional: used as mutation key
103
+ mutate: (params) => api.post('/users', params)
104
+ })
105
+ ```
106
+
107
+ ## Using Queries
108
+
109
+ ### Basic Query
110
+
111
+ ```typescript
112
+ function UserList() {
113
+ const [users, { isLoading, error, refetch }] = useQuery(usersCollection)
114
+
115
+ if (isLoading) return <Loading />
116
+ if (error) return <Error error={error} />
117
+
118
+ return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
119
+ }
120
+ ```
121
+
122
+ ### With Parameters
123
+
124
+ ```typescript
125
+ const [users] = useQuery(usersCollection, {
126
+ params: { page: 2 }
127
+ })
128
+ ```
129
+
130
+ ### Entity Query
131
+
132
+ ```typescript
133
+ const [profile] = useQuery(profileEntity, {
134
+ params: userId
135
+ })
136
+ ```
137
+
138
+ ### Paginated (Infinite) Query
139
+
140
+ ```typescript
141
+ const [posts, query, pagination] = useQuery(postsCollection, {
142
+ paginated: true,
143
+ getPageParams: ({ pageParam = 1 }) => ({ page: pageParam, limit: 10 })
144
+ })
145
+
146
+ // Load more
147
+ <button onClick={pagination.fetchNextPage} disabled={!pagination.hasNextPage}>
148
+ Load More
149
+ </button>
150
+ ```
151
+
152
+ ## Optimistic Updates with Channel API
153
+
154
+ The channel API provides an imperative way to apply optimistic updates:
155
+
156
+ ```typescript
157
+ const { mutate } = useMutation(createUser, {
158
+ optimistic: (channel, params) => {
159
+ // Add to collection immediately
160
+ channel(usersCollection).prepend({
161
+ id: 'temp-' + Date.now(),
162
+ name: params.name,
163
+ email: params.email,
164
+ }, { sync: true }) // sync: replace with server response
165
+ }
166
+ })
167
+ ```
168
+
169
+ ### Collection Operations
170
+
171
+ ```typescript
172
+ optimistic: (channel, params) => {
173
+ const ch = channel(usersCollection)
174
+
175
+ // Add items
176
+ ch.prepend(newItem, { sync: true })
177
+ ch.append(newItem, { sync: true })
178
+
179
+ // Update by ID
180
+ ch.update(params.id, user => ({
181
+ ...user,
182
+ name: params.newName
183
+ }))
184
+
185
+ // Delete by ID
186
+ ch.delete(params.id)
187
+ }
188
+ ```
189
+
190
+ ### Entity Operations
191
+
192
+ ```typescript
193
+ optimistic: (channel, params) => {
194
+ channel(profileEntity).update(profile => ({
195
+ ...profile,
196
+ name: params.newName
197
+ }))
198
+
199
+ // Or replace entirely
200
+ channel(profileEntity).replace(newProfile)
201
+ }
202
+ ```
203
+
204
+ ### Multiple Updates
205
+
206
+ Update multiple collections/entities in a single mutation:
207
+
208
+ ```typescript
209
+ const { mutate } = useMutation(deleteUser, {
210
+ optimistic: (channel, params) => {
211
+ // Remove from users list
212
+ channel(usersCollection).delete(params.userId)
213
+
214
+ // Update stats
215
+ channel(statsEntity).update(stats => ({
216
+ ...stats,
217
+ userCount: stats.userCount - 1
218
+ }))
219
+ }
220
+ })
221
+ ```
222
+
223
+ ## Standalone Channel (Outside Mutations)
224
+
225
+ Use the channel directly for immediate updates with manual rollback:
226
+
227
+ ```typescript
228
+ import { channel } from 'query-toolkit'
229
+
230
+ async function handleLike(postId: string) {
231
+ // Apply optimistic update immediately
232
+ const rollback = channel(postsCollection).update(postId, post => ({
233
+ ...post,
234
+ likes: post.likes + 1
235
+ }))
236
+
237
+ try {
238
+ await api.post(`/posts/${postId}/like`)
239
+ } catch (error) {
240
+ // Undo on failure
241
+ rollback()
242
+ }
243
+ }
244
+ ```
245
+
246
+ ## Optimistic Status
247
+
248
+ Items with pending optimistic updates include metadata for UI feedback:
249
+
250
+ ```typescript
251
+ {users?.map(user => (
252
+ <div
253
+ key={user.id}
254
+ style={{ opacity: user._optimistic?.status === 'pending' ? 0.5 : 1 }}
255
+ >
256
+ {user.name}
257
+ {user._optimistic?.status === 'error' && (
258
+ <span className="error">Failed to save</span>
259
+ )}
260
+ </div>
261
+ ))}
262
+ ```
263
+
264
+ ## API Reference
265
+
266
+ ### `defineCollection<TData, TParams>(config)`
267
+
268
+ | Property | Type | Description |
269
+ |----------|------|-------------|
270
+ | `name` | `string` | Unique identifier |
271
+ | `id` | `(item: TData) => string` | Extract ID from item |
272
+ | `fetch` | `(params: TParams) => Promise<TData[]>` | Fetch function |
273
+
274
+ ### `defineEntity<TData, TParams>(config)`
275
+
276
+ | Property | Type | Description |
277
+ |----------|------|-------------|
278
+ | `name` | `string` | Unique identifier |
279
+ | `fetch` | `(params: TParams) => Promise<TData>` | Fetch function |
280
+
281
+ ### `defineMutation<TParams, TResponse>(config)`
282
+
283
+ | Property | Type | Description |
284
+ |----------|------|-------------|
285
+ | `name?` | `string` | Optional mutation key |
286
+ | `mutate` | `(params: TParams) => Promise<TResponse>` | Mutation function |
287
+
288
+ ### `useQuery(def, options?)`
289
+
290
+ Returns `[data, queryState]` or `[data, queryState, paginationState]` for paginated queries.
291
+
292
+ **Options:**
293
+ - `params` - Parameters for fetch function
294
+ - `enabled` - Enable/disable query
295
+ - `staleTime` - Time before data is stale (ms)
296
+ - `refetchOnMount` - Refetch on component mount
297
+ - `refetchOnWindowFocus` - Refetch when window focuses
298
+ - `refetchInterval` - Polling interval (ms)
299
+ - `paginated` - Enable infinite query mode
300
+ - `getPageParams` - Transform page context to params
301
+
302
+ ### `useMutation(def, options?)`
303
+
304
+ Returns `{ mutate, mutateAsync, isPending, isError, isSuccess, error, data, reset }`.
305
+
306
+ **Options:**
307
+ - `optimistic` - `(channel, params) => void` - Apply optimistic updates
308
+ - `onMutate` - Called when mutation starts
309
+ - `onSuccess` - Called on success
310
+ - `onError` - Called on error
311
+
312
+ ### `channel(target)`
313
+
314
+ Standalone channel for immediate optimistic updates.
315
+
316
+ ```typescript
317
+ // For collections
318
+ channel(collection).prepend(item, { sync?: boolean })
319
+ channel(collection).append(item, { sync?: boolean })
320
+ channel(collection).update(id, updateFn, { sync?: boolean })
321
+ channel(collection).updateWhere(predicate, updateFn)
322
+ channel(collection).delete(id)
323
+ channel(collection).deleteWhere(predicate)
324
+
325
+ // For entities
326
+ channel(entity).update(updateFn, { sync?: boolean })
327
+ channel(entity).replace(data, { sync?: boolean })
328
+ ```
329
+
330
+ All methods return a rollback function.
331
+
332
+ ## Submodule Imports
333
+
334
+ ```typescript
335
+ // Core only (no React dependency)
336
+ import { defineCollection, defineEntity, defineMutation, channel } from 'query-toolkit/core'
337
+
338
+ // React hooks only
339
+ import { useQuery, useMutation } from 'query-toolkit/react'
340
+ ```
341
+
342
+ ## TypeScript
343
+
344
+ Full type inference from definitions:
345
+
346
+ ```typescript
347
+ const usersCollection = defineCollection<User, { page: number }>({...})
348
+ const createUser = defineMutation<CreateUserParams, User>({...})
349
+
350
+ // Types flow automatically
351
+ const [users] = useQuery(usersCollection, { params: { page: 1 } })
352
+ // users: User[] | undefined
353
+
354
+ const { mutate } = useMutation(createUser, {
355
+ optimistic: (channel, params) => {
356
+ // params: CreateUserParams
357
+ channel(usersCollection).prepend({...})
358
+ // Type-checks that data matches User
359
+ }
360
+ })
361
+ // mutate: (params: CreateUserParams) => void
362
+ ```
363
+
364
+ ## Peer Dependencies
365
+
366
+ - `react` >= 18.0.0
367
+ - `@tanstack/react-query` >= 5.0.0
368
+
369
+ ## License
370
+
371
+ MIT
@@ -0,0 +1,211 @@
1
+ import { I as IdGetter, C as CollectionDef, E as EntityDef, M as MutationDef, a as Optimistic } from '../types-BRxQA1mR.mjs';
2
+ export { A as AnyDef, b as OptimisticAction, c as OptimisticInstruction, O as OptimisticStatus, P as PaginatedOptions, Q as QueryOptions } from '../types-BRxQA1mR.mjs';
3
+
4
+ /**
5
+ * Define a collection query for fetching arrays of items
6
+ *
7
+ * @example
8
+ * const postsQuery = defineCollection({
9
+ * name: 'posts',
10
+ * id: (post) => post._id,
11
+ * fetch: ({ page }) => api.get(`/posts?page=${page}`).json()
12
+ * })
13
+ */
14
+ declare function defineCollection<TData, TParams = void>(config: {
15
+ name: string;
16
+ id: IdGetter<TData>;
17
+ fetch: (params: TParams) => Promise<TData[]>;
18
+ }): CollectionDef<TData, TParams>;
19
+ /**
20
+ * Define an entity for fetching single items
21
+ *
22
+ * @example
23
+ * const userEntity = defineEntity({
24
+ * name: 'user',
25
+ * fetch: (userId) => api.get(`/users/${userId}`).json()
26
+ * })
27
+ */
28
+ declare function defineEntity<TData, TParams = void>(config: {
29
+ name: string;
30
+ fetch: (params: TParams) => Promise<TData>;
31
+ }): EntityDef<TData, TParams>;
32
+ /**
33
+ * Define a mutation for writing data
34
+ *
35
+ * @example
36
+ * const createPost = defineMutation({
37
+ * name: 'createPost',
38
+ * mutate: (data) => api.post('/posts', { json: data }).json()
39
+ * })
40
+ */
41
+ declare function defineMutation<TParams, TResponse = void>(config: {
42
+ name?: string;
43
+ mutate: (params: TParams) => Promise<TResponse>;
44
+ }): MutationDef<TParams, TResponse>;
45
+
46
+ /** Registered collection entry */
47
+ interface RegisteredCollection<T = any> {
48
+ kind: 'collection';
49
+ name: string;
50
+ queryKey: readonly unknown[];
51
+ def: CollectionDef<T, any>;
52
+ getData: () => T[] | undefined;
53
+ setData: (updater: (prev: T[] | undefined) => T[] | undefined) => void;
54
+ }
55
+ /** Registered entity entry */
56
+ interface RegisteredEntity<T = any> {
57
+ kind: 'entity';
58
+ name: string;
59
+ queryKey: readonly unknown[];
60
+ def: EntityDef<T, any>;
61
+ getData: () => T | undefined;
62
+ setData: (updater: (prev: T | undefined) => T | undefined) => void;
63
+ }
64
+ /** Registered paginated collection entry */
65
+ interface RegisteredPaginatedCollection<T = any> {
66
+ kind: 'paginated';
67
+ name: string;
68
+ queryKey: readonly unknown[];
69
+ def: CollectionDef<T, any>;
70
+ getData: () => {
71
+ pages: T[][];
72
+ pageParams: unknown[];
73
+ } | undefined;
74
+ setData: (updater: (prev: {
75
+ pages: T[][];
76
+ pageParams: unknown[];
77
+ } | undefined) => {
78
+ pages: T[][];
79
+ pageParams: unknown[];
80
+ } | undefined) => void;
81
+ }
82
+ type RegisteredEntry = RegisteredCollection | RegisteredEntity | RegisteredPaginatedCollection;
83
+ /**
84
+ * Internal registry for tracking active queries
85
+ * Used by optimistic updates to broadcast changes
86
+ */
87
+ declare class QueryRegistry {
88
+ private entries;
89
+ /** Register an active query */
90
+ register(entry: RegisteredEntry): void;
91
+ /** Unregister a query when component unmounts */
92
+ unregister(entry: RegisteredEntry): void;
93
+ /** Get all registered entries for a query name */
94
+ getByName(name: string): RegisteredEntry[];
95
+ /** Apply an optimistic update to all queries with given name */
96
+ applyUpdate<T>(name: string, action: 'prepend' | 'append' | 'update' | 'delete' | 'replace', payload: {
97
+ data?: Partial<Optimistic<T>>;
98
+ id?: string;
99
+ where?: (item: T) => boolean;
100
+ update?: (item: T) => T;
101
+ }): (() => void)[];
102
+ private applyCollectionUpdate;
103
+ }
104
+ /** Singleton registry instance */
105
+ declare const registry: QueryRegistry;
106
+
107
+ /** Transaction returned from channel methods */
108
+ interface OptimisticTransaction {
109
+ target: CollectionDef<any, any> | EntityDef<any, any>;
110
+ action: 'prepend' | 'append' | 'update' | 'delete' | 'replace';
111
+ data?: any;
112
+ id?: string;
113
+ where?: (item: any) => boolean;
114
+ update?: (item: any) => any;
115
+ sync?: boolean;
116
+ rollback: () => void;
117
+ }
118
+ /** Channel for a collection - provides typed optimistic mutation methods */
119
+ declare class CollectionChannel<TEntity> {
120
+ private readonly target;
121
+ private readonly optimisticId;
122
+ constructor(target: CollectionDef<TEntity, any>);
123
+ /**
124
+ * Prepend an item to the collection
125
+ * @returns Rollback function to undo the change
126
+ */
127
+ prepend(data: TEntity, options?: {
128
+ sync?: boolean;
129
+ }): () => void;
130
+ /**
131
+ * Append an item to the collection
132
+ * @returns Rollback function to undo the change
133
+ */
134
+ append(data: TEntity, options?: {
135
+ sync?: boolean;
136
+ }): () => void;
137
+ /**
138
+ * Update an item in the collection by ID
139
+ * @returns Rollback function to undo the change
140
+ */
141
+ update(id: string, updateFn: (item: TEntity) => TEntity, options?: {
142
+ sync?: boolean;
143
+ }): () => void;
144
+ /**
145
+ * Update items matching a predicate
146
+ * @returns Rollback function to undo the change
147
+ */
148
+ updateWhere(where: (item: TEntity) => boolean, updateFn: (item: TEntity) => TEntity): () => void;
149
+ /**
150
+ * Delete an item from the collection by ID
151
+ * @returns Rollback function to undo the change
152
+ */
153
+ delete(id: string): () => void;
154
+ /**
155
+ * Delete items matching a predicate
156
+ * @returns Rollback function to undo the change
157
+ */
158
+ deleteWhere(where: (item: TEntity) => boolean): () => void;
159
+ }
160
+ /** Channel for an entity - provides typed optimistic mutation methods */
161
+ declare class EntityChannel<TEntity> {
162
+ private readonly target;
163
+ constructor(target: EntityDef<TEntity, any>);
164
+ /**
165
+ * Update the entity
166
+ * @returns Rollback function to undo the change
167
+ */
168
+ update(updateFn: (item: TEntity) => TEntity, options?: {
169
+ sync?: boolean;
170
+ }): () => void;
171
+ /**
172
+ * Replace the entity with new data
173
+ * @returns Rollback function to undo the change
174
+ */
175
+ replace(data: TEntity, options?: {
176
+ sync?: boolean;
177
+ }): () => void;
178
+ }
179
+ /**
180
+ * Channel function for optimistic mutations.
181
+ * Call with a collection or entity to get typed mutation methods.
182
+ *
183
+ * @example
184
+ * // Standalone usage
185
+ * const rollback = channel(usersCollection).prepend({ id: '1', name: 'John' });
186
+ * // Later, to undo:
187
+ * rollback();
188
+ *
189
+ * @example
190
+ * // Update an entity
191
+ * channel(userEntity).update(user => ({ ...user, name: 'Jane' }));
192
+ */
193
+ interface Channel {
194
+ <TEntity>(target: CollectionDef<TEntity, any>): CollectionChannel<TEntity>;
195
+ <TEntity>(target: EntityDef<TEntity, any>): EntityChannel<TEntity>;
196
+ }
197
+ /**
198
+ * Create a channel for optimistic mutations.
199
+ * Use this to apply immediate UI updates that can be rolled back.
200
+ *
201
+ * @example
202
+ * const rollback = channel(usersCollection).prepend(newUser);
203
+ * try {
204
+ * await api.createUser(newUser);
205
+ * } catch (error) {
206
+ * rollback(); // Undo the optimistic update
207
+ * }
208
+ */
209
+ declare const channel: Channel;
210
+
211
+ export { type Channel, CollectionChannel, CollectionDef, EntityChannel, EntityDef, IdGetter, MutationDef, Optimistic, type OptimisticTransaction, type RegisteredCollection, type RegisteredEntity, type RegisteredEntry, type RegisteredPaginatedCollection, channel, defineCollection, defineEntity, defineMutation, registry };