@topgunbuild/react 0.3.0 → 0.5.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/dist/index.d.mts +580 -4
- package/dist/index.d.ts +580 -4
- package/dist/index.js +381 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +381 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
- package/LICENSE +0 -97
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { ReactNode } from 'react';
|
|
2
2
|
import * as _topgunbuild_client from '@topgunbuild/client';
|
|
3
|
-
import { TopGunClient, QueryResultItem, QueryFilter, TopicCallback } from '@topgunbuild/client';
|
|
4
|
-
import { LWWMap, ORMap } from '@topgunbuild/core';
|
|
3
|
+
import { TopGunClient, ChangeEvent, QueryResultItem, QueryFilter, TopicCallback, RegisterResult, ResolverInfo } from '@topgunbuild/client';
|
|
4
|
+
import { LWWMap, ORMap, EntryProcessorResult, EntryProcessorDef, JournalEventType, JournalEvent, MergeRejection, ConflictResolverDef } from '@topgunbuild/core';
|
|
5
5
|
|
|
6
6
|
interface TopGunProviderProps {
|
|
7
7
|
client: TopGunClient;
|
|
@@ -10,12 +10,97 @@ interface TopGunProviderProps {
|
|
|
10
10
|
declare const TopGunProvider: React.FC<TopGunProviderProps>;
|
|
11
11
|
declare function useClient(): TopGunClient;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Options for useQuery change callbacks (Phase 5.1)
|
|
15
|
+
*/
|
|
16
|
+
interface UseQueryOptions<T> {
|
|
17
|
+
/** Called for any change event */
|
|
18
|
+
onChange?: (change: ChangeEvent<T>) => void;
|
|
19
|
+
/** Called when an item is added */
|
|
20
|
+
onAdd?: (key: string, value: T) => void;
|
|
21
|
+
/** Called when an item is updated */
|
|
22
|
+
onUpdate?: (key: string, value: T, previous: T) => void;
|
|
23
|
+
/** Called when an item is removed */
|
|
24
|
+
onRemove?: (key: string, previous: T) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Maximum number of changes to accumulate before auto-rotating.
|
|
27
|
+
* When exceeded, oldest changes are removed to prevent memory leaks.
|
|
28
|
+
* Default: 1000
|
|
29
|
+
*/
|
|
30
|
+
maxChanges?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Result type for useQuery hook with change tracking (Phase 5.1)
|
|
34
|
+
*/
|
|
13
35
|
interface UseQueryResult<T> {
|
|
36
|
+
/** Current data array */
|
|
14
37
|
data: QueryResultItem<T>[];
|
|
38
|
+
/** Loading state */
|
|
15
39
|
loading: boolean;
|
|
40
|
+
/** Error if query failed */
|
|
16
41
|
error: Error | null;
|
|
42
|
+
/** Last change event (Phase 5.1) */
|
|
43
|
+
lastChange: ChangeEvent<T> | null;
|
|
44
|
+
/** All changes since last clearChanges() call (Phase 5.1) */
|
|
45
|
+
changes: ChangeEvent<T>[];
|
|
46
|
+
/** Clear accumulated changes (Phase 5.1) */
|
|
47
|
+
clearChanges: () => void;
|
|
17
48
|
}
|
|
18
|
-
|
|
49
|
+
/**
|
|
50
|
+
* React hook for querying data with real-time updates and change tracking.
|
|
51
|
+
*
|
|
52
|
+
* @example Basic usage with change tracking
|
|
53
|
+
* ```tsx
|
|
54
|
+
* function TodoList() {
|
|
55
|
+
* const { data, lastChange } = useQuery<Todo>('todos');
|
|
56
|
+
*
|
|
57
|
+
* useEffect(() => {
|
|
58
|
+
* if (lastChange?.type === 'add') {
|
|
59
|
+
* toast.success(`New todo: ${lastChange.value.title}`);
|
|
60
|
+
* }
|
|
61
|
+
* }, [lastChange]);
|
|
62
|
+
*
|
|
63
|
+
* return <ul>{data.map(todo => <TodoItem key={todo._key} {...todo} />)}</ul>;
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example With callback-based notifications
|
|
68
|
+
* ```tsx
|
|
69
|
+
* function NotifyingTodoList() {
|
|
70
|
+
* const { data } = useQuery<Todo>('todos', undefined, {
|
|
71
|
+
* onAdd: (key, todo) => showNotification(`New: ${todo.title}`),
|
|
72
|
+
* onRemove: (key, todo) => showNotification(`Removed: ${todo.title}`)
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* return <ul>{data.map(todo => <TodoItem key={todo._key} {...todo} />)}</ul>;
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example With framer-motion animations
|
|
80
|
+
* ```tsx
|
|
81
|
+
* import { AnimatePresence, motion } from 'framer-motion';
|
|
82
|
+
*
|
|
83
|
+
* function AnimatedTodoList() {
|
|
84
|
+
* const { data } = useQuery<Todo>('todos');
|
|
85
|
+
*
|
|
86
|
+
* return (
|
|
87
|
+
* <AnimatePresence>
|
|
88
|
+
* {data.map(todo => (
|
|
89
|
+
* <motion.li
|
|
90
|
+
* key={todo._key}
|
|
91
|
+
* initial={{ opacity: 0, x: -20 }}
|
|
92
|
+
* animate={{ opacity: 1, x: 0 }}
|
|
93
|
+
* exit={{ opacity: 0, x: 20 }}
|
|
94
|
+
* >
|
|
95
|
+
* {todo.title}
|
|
96
|
+
* </motion.li>
|
|
97
|
+
* ))}
|
|
98
|
+
* </AnimatePresence>
|
|
99
|
+
* );
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
declare function useQuery<T = any>(mapName: string, query?: QueryFilter, options?: UseQueryOptions<T>): UseQueryResult<T>;
|
|
19
104
|
|
|
20
105
|
interface UseMutationResult<T, K = string> {
|
|
21
106
|
create: (key: K, value: T) => void;
|
|
@@ -31,4 +116,495 @@ declare function useORMap<K = string, V = any>(mapName: string): ORMap<K, V>;
|
|
|
31
116
|
|
|
32
117
|
declare function useTopic(topicName: string, callback?: TopicCallback): _topgunbuild_client.TopicHandle;
|
|
33
118
|
|
|
34
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Result type for usePNCounter hook.
|
|
121
|
+
*/
|
|
122
|
+
interface UsePNCounterResult {
|
|
123
|
+
/** Current counter value */
|
|
124
|
+
value: number;
|
|
125
|
+
/** Increment the counter by 1 */
|
|
126
|
+
increment: () => void;
|
|
127
|
+
/** Decrement the counter by 1 */
|
|
128
|
+
decrement: () => void;
|
|
129
|
+
/** Add delta (positive or negative) to the counter */
|
|
130
|
+
add: (delta: number) => void;
|
|
131
|
+
/** Loading state (true until first value received) */
|
|
132
|
+
loading: boolean;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* React hook for using a PN Counter with real-time updates.
|
|
136
|
+
*
|
|
137
|
+
* PN Counters support increment and decrement operations that work offline
|
|
138
|
+
* and sync to server when connected. They guarantee convergence across
|
|
139
|
+
* distributed nodes without coordination.
|
|
140
|
+
*
|
|
141
|
+
* @param name The counter name (e.g., 'likes:post-123')
|
|
142
|
+
* @returns Counter value and methods
|
|
143
|
+
*
|
|
144
|
+
* @example Basic usage
|
|
145
|
+
* ```tsx
|
|
146
|
+
* function LikeButton({ postId }: { postId: string }) {
|
|
147
|
+
* const { value, increment } = usePNCounter(`likes:${postId}`);
|
|
148
|
+
*
|
|
149
|
+
* return (
|
|
150
|
+
* <button onClick={increment}>
|
|
151
|
+
* ❤️ {value}
|
|
152
|
+
* </button>
|
|
153
|
+
* );
|
|
154
|
+
* }
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* @example Inventory control
|
|
158
|
+
* ```tsx
|
|
159
|
+
* function InventoryControl({ productId }: { productId: string }) {
|
|
160
|
+
* const { value, increment, decrement } = usePNCounter(`inventory:${productId}`);
|
|
161
|
+
*
|
|
162
|
+
* return (
|
|
163
|
+
* <div>
|
|
164
|
+
* <span>Stock: {value}</span>
|
|
165
|
+
* <button onClick={decrement} disabled={value <= 0}>-</button>
|
|
166
|
+
* <button onClick={increment}>+</button>
|
|
167
|
+
* </div>
|
|
168
|
+
* );
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @example Bulk operations
|
|
173
|
+
* ```tsx
|
|
174
|
+
* function BulkAdd({ counterId }: { counterId: string }) {
|
|
175
|
+
* const { value, add } = usePNCounter(counterId);
|
|
176
|
+
* const [amount, setAmount] = useState(10);
|
|
177
|
+
*
|
|
178
|
+
* return (
|
|
179
|
+
* <div>
|
|
180
|
+
* <span>Value: {value}</span>
|
|
181
|
+
* <input
|
|
182
|
+
* type="number"
|
|
183
|
+
* value={amount}
|
|
184
|
+
* onChange={(e) => setAmount(parseInt(e.target.value))}
|
|
185
|
+
* />
|
|
186
|
+
* <button onClick={() => add(amount)}>Add {amount}</button>
|
|
187
|
+
* <button onClick={() => add(-amount)}>Subtract {amount}</button>
|
|
188
|
+
* </div>
|
|
189
|
+
* );
|
|
190
|
+
* }
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
declare function usePNCounter(name: string): UsePNCounterResult;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Options for the useEntryProcessor hook.
|
|
197
|
+
*/
|
|
198
|
+
interface UseEntryProcessorOptions {
|
|
199
|
+
/**
|
|
200
|
+
* Number of retry attempts on failure.
|
|
201
|
+
* Default: 0 (no retries)
|
|
202
|
+
*/
|
|
203
|
+
retries?: number;
|
|
204
|
+
/**
|
|
205
|
+
* Delay between retries in milliseconds.
|
|
206
|
+
* Default: 100ms, doubles with each retry (exponential backoff)
|
|
207
|
+
*/
|
|
208
|
+
retryDelayMs?: number;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Result type for useEntryProcessor hook.
|
|
212
|
+
*/
|
|
213
|
+
interface UseEntryProcessorResult<R> {
|
|
214
|
+
/**
|
|
215
|
+
* Execute the processor on a key.
|
|
216
|
+
* @param key The key to process
|
|
217
|
+
* @param args Optional arguments to pass to the processor
|
|
218
|
+
*/
|
|
219
|
+
execute: (key: string, args?: unknown) => Promise<EntryProcessorResult<R>>;
|
|
220
|
+
/**
|
|
221
|
+
* Execute the processor on multiple keys.
|
|
222
|
+
* @param keys The keys to process
|
|
223
|
+
* @param args Optional arguments to pass to the processor
|
|
224
|
+
*/
|
|
225
|
+
executeMany: (keys: string[], args?: unknown) => Promise<Map<string, EntryProcessorResult<R>>>;
|
|
226
|
+
/** True while a processor is executing */
|
|
227
|
+
executing: boolean;
|
|
228
|
+
/** Last execution result (single key) */
|
|
229
|
+
lastResult: EntryProcessorResult<R> | null;
|
|
230
|
+
/** Last error encountered */
|
|
231
|
+
error: Error | null;
|
|
232
|
+
/** Reset the hook state (clears lastResult and error) */
|
|
233
|
+
reset: () => void;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* React hook for executing entry processors with loading and error states.
|
|
237
|
+
*
|
|
238
|
+
* Entry processors execute user-defined logic atomically on the server,
|
|
239
|
+
* solving the read-modify-write race condition.
|
|
240
|
+
*
|
|
241
|
+
* @param mapName Name of the map to operate on
|
|
242
|
+
* @param processorDef Processor definition (without args - args are passed per-execution)
|
|
243
|
+
* @param options Optional configuration
|
|
244
|
+
* @returns Execute function and state
|
|
245
|
+
*
|
|
246
|
+
* @example Basic increment
|
|
247
|
+
* ```tsx
|
|
248
|
+
* function LikeButton({ postId }: { postId: string }) {
|
|
249
|
+
* const { execute, executing } = useEntryProcessor<number>('likes', {
|
|
250
|
+
* name: 'increment',
|
|
251
|
+
* code: `
|
|
252
|
+
* const current = value ?? 0;
|
|
253
|
+
* return { value: current + 1, result: current + 1 };
|
|
254
|
+
* `,
|
|
255
|
+
* });
|
|
256
|
+
*
|
|
257
|
+
* const handleLike = async () => {
|
|
258
|
+
* const result = await execute(postId);
|
|
259
|
+
* if (result.success) {
|
|
260
|
+
* console.log('New like count:', result.result);
|
|
261
|
+
* }
|
|
262
|
+
* };
|
|
263
|
+
*
|
|
264
|
+
* return (
|
|
265
|
+
* <button onClick={handleLike} disabled={executing}>
|
|
266
|
+
* {executing ? '...' : 'Like'}
|
|
267
|
+
* </button>
|
|
268
|
+
* );
|
|
269
|
+
* }
|
|
270
|
+
* ```
|
|
271
|
+
*
|
|
272
|
+
* @example Inventory reservation with args
|
|
273
|
+
* ```tsx
|
|
274
|
+
* function ReserveButton({ productId }: { productId: string }) {
|
|
275
|
+
* const { execute, executing, error } = useEntryProcessor<
|
|
276
|
+
* { stock: number; reserved: string[] },
|
|
277
|
+
* { success: boolean; remaining: number }
|
|
278
|
+
* >('inventory', {
|
|
279
|
+
* name: 'reserve_item',
|
|
280
|
+
* code: `
|
|
281
|
+
* if (!value || value.stock <= 0) {
|
|
282
|
+
* return { value, result: { success: false, remaining: 0 } };
|
|
283
|
+
* }
|
|
284
|
+
* const newValue = {
|
|
285
|
+
* ...value,
|
|
286
|
+
* stock: value.stock - 1,
|
|
287
|
+
* reserved: [...value.reserved, args.userId],
|
|
288
|
+
* };
|
|
289
|
+
* return {
|
|
290
|
+
* value: newValue,
|
|
291
|
+
* result: { success: true, remaining: newValue.stock }
|
|
292
|
+
* };
|
|
293
|
+
* `,
|
|
294
|
+
* });
|
|
295
|
+
*
|
|
296
|
+
* const handleReserve = async () => {
|
|
297
|
+
* const result = await execute(productId, { userId: currentUser.id });
|
|
298
|
+
* if (result.success && result.result?.success) {
|
|
299
|
+
* toast.success(`Reserved! ${result.result.remaining} left`);
|
|
300
|
+
* } else {
|
|
301
|
+
* toast.error('Out of stock');
|
|
302
|
+
* }
|
|
303
|
+
* };
|
|
304
|
+
*
|
|
305
|
+
* return (
|
|
306
|
+
* <button onClick={handleReserve} disabled={executing}>
|
|
307
|
+
* {executing ? 'Reserving...' : 'Reserve'}
|
|
308
|
+
* </button>
|
|
309
|
+
* );
|
|
310
|
+
* }
|
|
311
|
+
* ```
|
|
312
|
+
*
|
|
313
|
+
* @example Using built-in processor
|
|
314
|
+
* ```tsx
|
|
315
|
+
* import { BuiltInProcessors } from '@topgunbuild/core';
|
|
316
|
+
*
|
|
317
|
+
* function DecrementStock({ productId }: { productId: string }) {
|
|
318
|
+
* const processorDef = useMemo(
|
|
319
|
+
* () => BuiltInProcessors.DECREMENT_FLOOR(1),
|
|
320
|
+
* []
|
|
321
|
+
* );
|
|
322
|
+
*
|
|
323
|
+
* const { execute, executing, lastResult } = useEntryProcessor<
|
|
324
|
+
* number,
|
|
325
|
+
* { newValue: number; wasFloored: boolean }
|
|
326
|
+
* >('stock', processorDef);
|
|
327
|
+
*
|
|
328
|
+
* const handleDecrement = async () => {
|
|
329
|
+
* const result = await execute(productId);
|
|
330
|
+
* if (result.result?.wasFloored) {
|
|
331
|
+
* alert('Stock is now at zero!');
|
|
332
|
+
* }
|
|
333
|
+
* };
|
|
334
|
+
*
|
|
335
|
+
* return (
|
|
336
|
+
* <button onClick={handleDecrement} disabled={executing}>
|
|
337
|
+
* Decrease Stock
|
|
338
|
+
* </button>
|
|
339
|
+
* );
|
|
340
|
+
* }
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
declare function useEntryProcessor<V = unknown, R = V>(mapName: string, processorDef: Omit<EntryProcessorDef<V, R>, 'args'>, options?: UseEntryProcessorOptions): UseEntryProcessorResult<R>;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Options for useEventJournal hook.
|
|
347
|
+
*/
|
|
348
|
+
interface UseEventJournalOptions {
|
|
349
|
+
/** Start from specific sequence */
|
|
350
|
+
fromSequence?: bigint;
|
|
351
|
+
/** Filter by map name */
|
|
352
|
+
mapName?: string;
|
|
353
|
+
/** Filter by event types */
|
|
354
|
+
types?: JournalEventType[];
|
|
355
|
+
/** Maximum events to keep in state (default: 100) */
|
|
356
|
+
maxEvents?: number;
|
|
357
|
+
/** Called when new event is received */
|
|
358
|
+
onEvent?: (event: JournalEvent) => void;
|
|
359
|
+
/** Pause subscription */
|
|
360
|
+
paused?: boolean;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Result type for useEventJournal hook.
|
|
364
|
+
*/
|
|
365
|
+
interface UseEventJournalResult {
|
|
366
|
+
/** Array of recent events (newest last) */
|
|
367
|
+
events: JournalEvent[];
|
|
368
|
+
/** Last received event */
|
|
369
|
+
lastEvent: JournalEvent | null;
|
|
370
|
+
/** Clear accumulated events */
|
|
371
|
+
clearEvents: () => void;
|
|
372
|
+
/** Read historical events from sequence */
|
|
373
|
+
readFrom: (sequence: bigint, limit?: number) => Promise<JournalEvent[]>;
|
|
374
|
+
/** Get latest sequence number */
|
|
375
|
+
getLatestSequence: () => Promise<bigint>;
|
|
376
|
+
/** Whether subscription is active */
|
|
377
|
+
isSubscribed: boolean;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* React hook for subscribing to Event Journal changes.
|
|
381
|
+
*
|
|
382
|
+
* The Event Journal captures all map changes (PUT, UPDATE, DELETE) as an
|
|
383
|
+
* append-only log, useful for:
|
|
384
|
+
* - Real-time activity feeds
|
|
385
|
+
* - Audit trails
|
|
386
|
+
* - Change notifications
|
|
387
|
+
* - Debugging and monitoring
|
|
388
|
+
*
|
|
389
|
+
* @example Basic usage - show all changes
|
|
390
|
+
* ```tsx
|
|
391
|
+
* function ActivityFeed() {
|
|
392
|
+
* const { events, lastEvent } = useEventJournal();
|
|
393
|
+
*
|
|
394
|
+
* return (
|
|
395
|
+
* <ul>
|
|
396
|
+
* {events.map((e) => (
|
|
397
|
+
* <li key={e.sequence.toString()}>
|
|
398
|
+
* {e.type} {e.mapName}:{e.key}
|
|
399
|
+
* </li>
|
|
400
|
+
* ))}
|
|
401
|
+
* </ul>
|
|
402
|
+
* );
|
|
403
|
+
* }
|
|
404
|
+
* ```
|
|
405
|
+
*
|
|
406
|
+
* @example Filter by map name
|
|
407
|
+
* ```tsx
|
|
408
|
+
* function UserActivityFeed() {
|
|
409
|
+
* const { events } = useEventJournal({ mapName: 'users' });
|
|
410
|
+
*
|
|
411
|
+
* return (
|
|
412
|
+
* <ul>
|
|
413
|
+
* {events.map((e) => (
|
|
414
|
+
* <li key={e.sequence.toString()}>
|
|
415
|
+
* User {e.key}: {e.type}
|
|
416
|
+
* </li>
|
|
417
|
+
* ))}
|
|
418
|
+
* </ul>
|
|
419
|
+
* );
|
|
420
|
+
* }
|
|
421
|
+
* ```
|
|
422
|
+
*
|
|
423
|
+
* @example With event callback
|
|
424
|
+
* ```tsx
|
|
425
|
+
* function NotifyingComponent() {
|
|
426
|
+
* const { events } = useEventJournal({
|
|
427
|
+
* mapName: 'orders',
|
|
428
|
+
* types: ['PUT'],
|
|
429
|
+
* onEvent: (event) => {
|
|
430
|
+
* toast.success(`New order: ${event.key}`);
|
|
431
|
+
* },
|
|
432
|
+
* });
|
|
433
|
+
*
|
|
434
|
+
* return <OrderList events={events} />;
|
|
435
|
+
* }
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
declare function useEventJournal(options?: UseEventJournalOptions): UseEventJournalResult;
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Options for useMergeRejections hook.
|
|
442
|
+
*/
|
|
443
|
+
interface UseMergeRejectionsOptions {
|
|
444
|
+
/** Filter rejections by map name (optional) */
|
|
445
|
+
mapName?: string;
|
|
446
|
+
/** Maximum number of rejections to keep in history */
|
|
447
|
+
maxHistory?: number;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Result type for useMergeRejections hook.
|
|
451
|
+
*/
|
|
452
|
+
interface UseMergeRejectionsResult {
|
|
453
|
+
/** List of recent merge rejections */
|
|
454
|
+
rejections: MergeRejection[];
|
|
455
|
+
/** Last rejection received */
|
|
456
|
+
lastRejection: MergeRejection | null;
|
|
457
|
+
/** Clear rejection history */
|
|
458
|
+
clear: () => void;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* React hook for subscribing to merge rejection events.
|
|
462
|
+
*
|
|
463
|
+
* Merge rejections occur when a custom conflict resolver rejects
|
|
464
|
+
* a client's write operation. This hook allows you to:
|
|
465
|
+
* - Display rejection notifications to users
|
|
466
|
+
* - Refresh local state after rejection
|
|
467
|
+
* - Log conflicts for debugging
|
|
468
|
+
*
|
|
469
|
+
* @param options Optional filtering and configuration
|
|
470
|
+
* @returns Rejection list and utilities
|
|
471
|
+
*
|
|
472
|
+
* @example Show rejection notifications
|
|
473
|
+
* ```tsx
|
|
474
|
+
* function BookingForm() {
|
|
475
|
+
* const { lastRejection, clear } = useMergeRejections({
|
|
476
|
+
* mapName: 'bookings'
|
|
477
|
+
* });
|
|
478
|
+
*
|
|
479
|
+
* useEffect(() => {
|
|
480
|
+
* if (lastRejection) {
|
|
481
|
+
* toast.error(`Booking failed: ${lastRejection.reason}`);
|
|
482
|
+
* clear(); // Clear after showing notification
|
|
483
|
+
* }
|
|
484
|
+
* }, [lastRejection]);
|
|
485
|
+
*
|
|
486
|
+
* return <form>...</form>;
|
|
487
|
+
* }
|
|
488
|
+
* ```
|
|
489
|
+
*
|
|
490
|
+
* @example Track all rejections
|
|
491
|
+
* ```tsx
|
|
492
|
+
* function ConflictLog() {
|
|
493
|
+
* const { rejections } = useMergeRejections({ maxHistory: 50 });
|
|
494
|
+
*
|
|
495
|
+
* return (
|
|
496
|
+
* <ul>
|
|
497
|
+
* {rejections.map((r, i) => (
|
|
498
|
+
* <li key={i}>
|
|
499
|
+
* {r.mapName}/{r.key}: {r.reason}
|
|
500
|
+
* </li>
|
|
501
|
+
* ))}
|
|
502
|
+
* </ul>
|
|
503
|
+
* );
|
|
504
|
+
* }
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
declare function useMergeRejections(options?: UseMergeRejectionsOptions): UseMergeRejectionsResult;
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Options for useConflictResolver hook.
|
|
511
|
+
*/
|
|
512
|
+
interface UseConflictResolverOptions {
|
|
513
|
+
/** Auto-unregister resolver on unmount (default: true) */
|
|
514
|
+
autoUnregister?: boolean;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Result type for useConflictResolver hook.
|
|
518
|
+
*/
|
|
519
|
+
interface UseConflictResolverResult {
|
|
520
|
+
/**
|
|
521
|
+
* Register a conflict resolver on the server.
|
|
522
|
+
* @param resolver The resolver definition
|
|
523
|
+
*/
|
|
524
|
+
register: (resolver: Omit<ConflictResolverDef, 'fn'>) => Promise<RegisterResult>;
|
|
525
|
+
/**
|
|
526
|
+
* Unregister a resolver by name.
|
|
527
|
+
* @param resolverName Name of the resolver to unregister
|
|
528
|
+
*/
|
|
529
|
+
unregister: (resolverName: string) => Promise<RegisterResult>;
|
|
530
|
+
/**
|
|
531
|
+
* List all registered resolvers for this map.
|
|
532
|
+
*/
|
|
533
|
+
list: () => Promise<ResolverInfo[]>;
|
|
534
|
+
/** True while a registration/unregistration is in progress */
|
|
535
|
+
loading: boolean;
|
|
536
|
+
/** Last error encountered */
|
|
537
|
+
error: Error | null;
|
|
538
|
+
/** List of resolvers registered by this hook instance */
|
|
539
|
+
registered: string[];
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* React hook for managing conflict resolvers on a specific map.
|
|
543
|
+
*
|
|
544
|
+
* Conflict resolvers allow you to customize how merge conflicts are handled
|
|
545
|
+
* on the server. This hook provides a convenient way to:
|
|
546
|
+
* - Register custom resolvers
|
|
547
|
+
* - Auto-unregister on component unmount
|
|
548
|
+
* - Track registration state
|
|
549
|
+
*
|
|
550
|
+
* @param mapName Name of the map to manage resolvers for
|
|
551
|
+
* @param options Optional configuration
|
|
552
|
+
* @returns Resolver management functions and state
|
|
553
|
+
*
|
|
554
|
+
* @example First-write-wins for bookings
|
|
555
|
+
* ```tsx
|
|
556
|
+
* function BookingManager() {
|
|
557
|
+
* const { register, registered, loading, error } = useConflictResolver('bookings');
|
|
558
|
+
*
|
|
559
|
+
* useEffect(() => {
|
|
560
|
+
* // Register resolver on mount
|
|
561
|
+
* register({
|
|
562
|
+
* name: 'first-write-wins',
|
|
563
|
+
* code: `
|
|
564
|
+
* if (context.localValue !== undefined) {
|
|
565
|
+
* return { action: 'reject', reason: 'Already booked' };
|
|
566
|
+
* }
|
|
567
|
+
* return { action: 'accept', value: context.remoteValue };
|
|
568
|
+
* `,
|
|
569
|
+
* priority: 100,
|
|
570
|
+
* });
|
|
571
|
+
* }, []);
|
|
572
|
+
*
|
|
573
|
+
* return (
|
|
574
|
+
* <div>
|
|
575
|
+
* {loading && <span>Registering...</span>}
|
|
576
|
+
* {error && <span>Error: {error.message}</span>}
|
|
577
|
+
* <ul>
|
|
578
|
+
* {registered.map(name => <li key={name}>{name}</li>)}
|
|
579
|
+
* </ul>
|
|
580
|
+
* </div>
|
|
581
|
+
* );
|
|
582
|
+
* }
|
|
583
|
+
* ```
|
|
584
|
+
*
|
|
585
|
+
* @example Numeric constraints
|
|
586
|
+
* ```tsx
|
|
587
|
+
* function InventorySettings() {
|
|
588
|
+
* const { register } = useConflictResolver('inventory');
|
|
589
|
+
*
|
|
590
|
+
* const enableNonNegative = async () => {
|
|
591
|
+
* await register({
|
|
592
|
+
* name: 'non-negative',
|
|
593
|
+
* code: `
|
|
594
|
+
* if (context.remoteValue < 0) {
|
|
595
|
+
* return { action: 'reject', reason: 'Stock cannot be negative' };
|
|
596
|
+
* }
|
|
597
|
+
* return { action: 'accept', value: context.remoteValue };
|
|
598
|
+
* `,
|
|
599
|
+
* priority: 90,
|
|
600
|
+
* keyPattern: 'stock:*',
|
|
601
|
+
* });
|
|
602
|
+
* };
|
|
603
|
+
*
|
|
604
|
+
* return <button onClick={enableNonNegative}>Enable Stock Protection</button>;
|
|
605
|
+
* }
|
|
606
|
+
* ```
|
|
607
|
+
*/
|
|
608
|
+
declare function useConflictResolver(mapName: string, options?: UseConflictResolverOptions): UseConflictResolverResult;
|
|
609
|
+
|
|
610
|
+
export { TopGunProvider, type TopGunProviderProps, type UseConflictResolverOptions, type UseConflictResolverResult, type UseEntryProcessorOptions, type UseEntryProcessorResult, type UseEventJournalOptions, type UseEventJournalResult, type UseMergeRejectionsOptions, type UseMergeRejectionsResult, type UseMutationResult, type UsePNCounterResult, type UseQueryOptions, type UseQueryResult, useClient, useConflictResolver, useEntryProcessor, useEventJournal, useMap, useMergeRejections, useMutation, useORMap, usePNCounter, useQuery, useTopic };
|