@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.ts 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
- declare function useQuery<T = any>(mapName: string, query?: QueryFilter): UseQueryResult<T>;
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
- export { TopGunProvider, type TopGunProviderProps, type UseMutationResult, type UseQueryResult, useClient, useMap, useMutation, useORMap, useQuery, useTopic };
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 };