@standardbeagle/data-router 1.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 +134 -0
- package/dist/FormHandler.d.ts +19 -0
- package/dist/hooks/useAsyncBatch.d.ts +46 -0
- package/dist/hooks/useAsyncData.d.ts +23 -0
- package/dist/hooks/useAsyncMutation.d.ts +22 -0
- package/dist/hooks/useAsyncState.d.ts +44 -0
- package/dist/hooks/useData.d.ts +3 -0
- package/dist/hooks/useDataManipulation.d.ts +2 -0
- package/dist/hooks/useInvalidation.d.ts +28 -0
- package/dist/hooks/useNavigate.d.ts +4 -0
- package/dist/hooks/useOptimisticUpdates.d.ts +27 -0
- package/dist/hooks/useXPath.d.ts +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.es.d.ts +1 -0
- package/dist/index.es.js +2904 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.cjs +11 -0
- package/dist/index.umd.cjs.map +1 -0
- package/dist/provider.d.ts +12 -0
- package/dist/state/actions.d.ts +23 -0
- package/dist/state/command-queue.d.ts +18 -0
- package/dist/state/reducer.d.ts +3 -0
- package/dist/state/xpath-utils.d.ts +10 -0
- package/dist/types.d.ts +135 -0
- package/package.json +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @standardbeagle/data-router
|
|
2
|
+
|
|
3
|
+
A memory-only, XPath-based router for React that operates with data structures instead of URLs. Perfect for complex React components that need internal routing and data manipulation without interfering with the parent application's routing system.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **XPath Navigation**: Navigate through data structures using XPath expressions
|
|
8
|
+
- **Data Manipulation**: Built-in CRUD operations (merge, replace, append, delete)
|
|
9
|
+
- **Form Integration**: Form components that automatically update data at specified XPaths
|
|
10
|
+
- **Memory-only**: No browser URL integration - purely internal routing
|
|
11
|
+
- **TypeScript Support**: Full TypeScript support with strict typing
|
|
12
|
+
- **React Hooks**: Hook-based API for easy integration
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @standardbeagle/data-router
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Basic Usage
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { DataProvider, useXPath, useData, Link, Form, Button } from '@standardbeagle/data-router';
|
|
24
|
+
|
|
25
|
+
// Initialize with some data
|
|
26
|
+
const initialData = {
|
|
27
|
+
users: [
|
|
28
|
+
{ id: 1, name: 'John', profile: { email: 'john@example.com' } }
|
|
29
|
+
],
|
|
30
|
+
settings: { theme: 'light' }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function App() {
|
|
34
|
+
return (
|
|
35
|
+
<DataProvider initialData={initialData} initialXPath="/users[0]">
|
|
36
|
+
<UserProfile />
|
|
37
|
+
</DataProvider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function UserProfile() {
|
|
42
|
+
const xpath = useXPath(); // "/users[0]"
|
|
43
|
+
const targetData = useTargetData(); // { id: 1, name: 'John', profile: {...} }
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
<h1>Current Location: {xpath}</h1>
|
|
48
|
+
<p>User: {targetData.name}</p>
|
|
49
|
+
|
|
50
|
+
{/* Navigate to user's profile */}
|
|
51
|
+
<Link to="profile">View Profile</Link>
|
|
52
|
+
|
|
53
|
+
{/* Navigate to settings */}
|
|
54
|
+
<Link to="/settings">Go to Settings</Link>
|
|
55
|
+
|
|
56
|
+
{/* Form to update user data */}
|
|
57
|
+
<Form targetXPath="/users[0]" operation="merge">
|
|
58
|
+
<input name="name" defaultValue={targetData.name} />
|
|
59
|
+
<Button dataAction="merge">Update User</Button>
|
|
60
|
+
</Form>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Core Concepts
|
|
67
|
+
|
|
68
|
+
### XPath Navigation
|
|
69
|
+
- **Absolute paths**: `/users[0]/profile/email`
|
|
70
|
+
- **Relative paths**: `profile/settings` (relative to current XPath)
|
|
71
|
+
- **Parent navigation**: `..` (go up one level), `../..` (go up two levels)
|
|
72
|
+
|
|
73
|
+
### Data Operations
|
|
74
|
+
- **merge**: Combine form data with existing object
|
|
75
|
+
- **replace**: Replace entire object/value at XPath
|
|
76
|
+
- **append**: Add to arrays or create new properties
|
|
77
|
+
- **delete**: Remove properties/array elements
|
|
78
|
+
|
|
79
|
+
### Components
|
|
80
|
+
|
|
81
|
+
#### DataProvider
|
|
82
|
+
Provides the data context and routing state.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
<DataProvider
|
|
86
|
+
initialData={myData}
|
|
87
|
+
initialXPath="/users[0]"
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
</DataProvider>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Link
|
|
94
|
+
Navigate to different XPaths (read-only navigation).
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
<Link to="/users[1]">Go to User 1</Link>
|
|
98
|
+
<Link to="../settings">Go to Settings</Link>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### Form
|
|
102
|
+
Wrap form elements for data manipulation.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<Form targetXPath="/users[0]" operation="merge">
|
|
106
|
+
<input name="email" />
|
|
107
|
+
<Button dataAction="merge">Save</Button>
|
|
108
|
+
</Form>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Button
|
|
112
|
+
Submit button with data manipulation capabilities.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
<Button
|
|
116
|
+
dataAction="replace"
|
|
117
|
+
targetXPath="/settings/theme"
|
|
118
|
+
navigateTo="/settings"
|
|
119
|
+
>
|
|
120
|
+
Update Theme
|
|
121
|
+
</Button>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Hooks
|
|
125
|
+
|
|
126
|
+
- `useXPath()` - Get current XPath
|
|
127
|
+
- `useData()` - Get entire data object
|
|
128
|
+
- `useTargetData()` - Get data at current XPath
|
|
129
|
+
- `useNavigate()` - Get navigation function
|
|
130
|
+
- `useDataManipulation()` - Get data manipulation functions
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
ISC
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ReactNode, ButtonHTMLAttributes } from 'react';
|
|
2
|
+
import { FormData, DataOperationType } from './types';
|
|
3
|
+
interface FormHandlerProps {
|
|
4
|
+
xpath?: string;
|
|
5
|
+
operation?: DataOperationType;
|
|
6
|
+
onSubmit?: (data: FormData, navigate: (xpath: string) => void) => void;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function Form({ xpath, operation, onSubmit, children, ...props }: FormHandlerProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
13
|
+
dataAction?: DataOperationType;
|
|
14
|
+
targetXPath?: string;
|
|
15
|
+
navigateTo?: string;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export declare function Button({ dataAction, targetXPath, navigateTo, children, onClick, type, ...props }: ButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface BatchOperation<T = any> {
|
|
2
|
+
xpath: string;
|
|
3
|
+
fetcher: () => Promise<T>;
|
|
4
|
+
priority?: 'low' | 'normal' | 'high';
|
|
5
|
+
onSuccess?: (data: T) => void;
|
|
6
|
+
onError?: (error: Error) => void;
|
|
7
|
+
}
|
|
8
|
+
export interface BatchConfig {
|
|
9
|
+
concurrency?: number;
|
|
10
|
+
failFast?: boolean;
|
|
11
|
+
retryCount?: number;
|
|
12
|
+
retryDelay?: number;
|
|
13
|
+
onBatchComplete?: (results: BatchResult[]) => void;
|
|
14
|
+
onBatchError?: (error: Error, results: BatchResult[]) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface BatchResult<T = any> {
|
|
17
|
+
xpath: string;
|
|
18
|
+
status: 'success' | 'error' | 'pending';
|
|
19
|
+
data?: T;
|
|
20
|
+
error?: Error;
|
|
21
|
+
}
|
|
22
|
+
export interface AsyncBatchResult {
|
|
23
|
+
execute: () => Promise<BatchResult[]>;
|
|
24
|
+
cancel: () => void;
|
|
25
|
+
isLoading: boolean;
|
|
26
|
+
isError: boolean;
|
|
27
|
+
isSuccess: boolean;
|
|
28
|
+
isIdle: boolean;
|
|
29
|
+
results: BatchResult[];
|
|
30
|
+
progress: {
|
|
31
|
+
total: number;
|
|
32
|
+
completed: number;
|
|
33
|
+
failed: number;
|
|
34
|
+
pending: number;
|
|
35
|
+
percentage: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare function useAsyncBatch(operations: BatchOperation[], config?: BatchConfig): AsyncBatchResult;
|
|
39
|
+
export declare function useAsyncParallel<T = any>(operations: Array<{
|
|
40
|
+
xpath: string;
|
|
41
|
+
fetcher: () => Promise<T>;
|
|
42
|
+
}>, config?: Omit<BatchConfig, 'concurrency'>): AsyncBatchResult;
|
|
43
|
+
export declare function useAsyncSequential<T = any>(operations: Array<{
|
|
44
|
+
xpath: string;
|
|
45
|
+
fetcher: () => Promise<T>;
|
|
46
|
+
}>, config?: Omit<BatchConfig, 'concurrency'>): AsyncBatchResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface AsyncDataConfig {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
staleTime?: number;
|
|
4
|
+
cacheTime?: number;
|
|
5
|
+
refetchOnWindowFocus?: boolean;
|
|
6
|
+
refetchOnReconnect?: boolean;
|
|
7
|
+
retryCount?: number;
|
|
8
|
+
retryDelay?: number;
|
|
9
|
+
onSuccess?: (data: any) => void;
|
|
10
|
+
onError?: (error: Error) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface AsyncDataResult<T = any> {
|
|
13
|
+
data: T | undefined;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
isError: boolean;
|
|
16
|
+
isSuccess: boolean;
|
|
17
|
+
isIdle: boolean;
|
|
18
|
+
error: Error | undefined;
|
|
19
|
+
refetch: () => Promise<T>;
|
|
20
|
+
cancel: () => void;
|
|
21
|
+
invalidate: () => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function useAsyncData<T = any>(xpath: string, fetcher: () => Promise<T>, config?: AsyncDataConfig): AsyncDataResult<T>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface AsyncMutationConfig<TData = any, TVariables = any> {
|
|
2
|
+
optimisticUpdate?: (currentData: TData, variables: TVariables) => TData;
|
|
3
|
+
rollbackOnError?: boolean;
|
|
4
|
+
invalidate?: string[];
|
|
5
|
+
retryCount?: number;
|
|
6
|
+
retryDelay?: number;
|
|
7
|
+
onSuccess?: (data: TData, variables: TVariables) => void;
|
|
8
|
+
onError?: (error: Error, variables: TVariables) => void;
|
|
9
|
+
onSettled?: (data: TData | undefined, error: Error | undefined, variables: TVariables) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface AsyncMutationResult<TData = any, TVariables = any> {
|
|
12
|
+
mutate: (variables: TVariables) => Promise<TData>;
|
|
13
|
+
mutateAsync: (variables: TVariables) => Promise<TData>;
|
|
14
|
+
reset: () => void;
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
isError: boolean;
|
|
17
|
+
isSuccess: boolean;
|
|
18
|
+
isIdle: boolean;
|
|
19
|
+
data: TData | undefined;
|
|
20
|
+
error: Error | undefined;
|
|
21
|
+
}
|
|
22
|
+
export declare function useAsyncMutation<TData = any, TVariables = any>(xpath: string, mutationFn: (variables: TVariables) => Promise<TData>, config?: AsyncMutationConfig<TData, TVariables>): AsyncMutationResult<TData, TVariables>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AsyncState } from '../types';
|
|
2
|
+
export interface AsyncStateInfo extends AsyncState {
|
|
3
|
+
xpath: string;
|
|
4
|
+
hasPendingOperation: boolean;
|
|
5
|
+
isStale: (staleTime?: number) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface AsyncStateResult<T = any> {
|
|
8
|
+
data: T | undefined;
|
|
9
|
+
asyncState: AsyncStateInfo | undefined;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
isError: boolean;
|
|
12
|
+
isSuccess: boolean;
|
|
13
|
+
isIdle: boolean;
|
|
14
|
+
error: Error | undefined;
|
|
15
|
+
isPending: boolean;
|
|
16
|
+
lastUpdated: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare function useAsyncState<T = any>(xpath: string): AsyncStateResult<T>;
|
|
19
|
+
export declare function useAsyncStates(xpaths: string[]): Record<string, AsyncStateResult>;
|
|
20
|
+
export declare function useGlobalAsyncState(): {
|
|
21
|
+
summary: {
|
|
22
|
+
totalStates: number;
|
|
23
|
+
loadingStates: number;
|
|
24
|
+
errorStates: number;
|
|
25
|
+
successStates: number;
|
|
26
|
+
idleStates: number;
|
|
27
|
+
pendingOperations: number;
|
|
28
|
+
executingOperations: number;
|
|
29
|
+
queuedOperations: number;
|
|
30
|
+
hasAnyLoading: boolean;
|
|
31
|
+
hasAnyError: boolean;
|
|
32
|
+
optimisticUpdateCount: number;
|
|
33
|
+
};
|
|
34
|
+
statesByStatus: {
|
|
35
|
+
loading: [string, AsyncState][];
|
|
36
|
+
error: [string, AsyncState][];
|
|
37
|
+
success: [string, AsyncState][];
|
|
38
|
+
idle: [string, AsyncState][];
|
|
39
|
+
};
|
|
40
|
+
asyncStates: Record<string, AsyncState>;
|
|
41
|
+
pendingOperations: ReadonlySet<string>;
|
|
42
|
+
commandQueue: import("../types").CommandQueue;
|
|
43
|
+
optimisticUpdates: Record<string, import("../types").OptimisticUpdate>;
|
|
44
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface InvalidationConfig {
|
|
2
|
+
includeChildren?: boolean;
|
|
3
|
+
includeParents?: boolean;
|
|
4
|
+
clearData?: boolean;
|
|
5
|
+
force?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface InvalidationResult {
|
|
8
|
+
invalidate: (xpath: string, config?: InvalidationConfig) => void;
|
|
9
|
+
invalidateMany: (xpaths: string[], config?: InvalidationConfig) => void;
|
|
10
|
+
invalidatePattern: (pattern: string, config?: InvalidationConfig) => void;
|
|
11
|
+
invalidateAll: () => void;
|
|
12
|
+
revalidate: (xpath: string) => void;
|
|
13
|
+
revalidateMany: (xpaths: string[]) => void;
|
|
14
|
+
getInvalidationCount: () => number;
|
|
15
|
+
}
|
|
16
|
+
export declare function useInvalidation(): InvalidationResult;
|
|
17
|
+
export declare function useAutoInvalidation(dependencies: string[], config?: InvalidationConfig & {
|
|
18
|
+
debounceMs?: number;
|
|
19
|
+
onInvalidate?: (paths: string[]) => void;
|
|
20
|
+
}): {
|
|
21
|
+
trigger: () => void;
|
|
22
|
+
invalidateDependencies: () => void;
|
|
23
|
+
};
|
|
24
|
+
export declare function useQueryInvalidation(): {
|
|
25
|
+
invalidateQueries: (queryKey: string | string[]) => void;
|
|
26
|
+
invalidateQueriesMatching: (predicate: (path: string) => boolean) => void;
|
|
27
|
+
getQueries: (queryKey?: string | string[]) => string[];
|
|
28
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface OptimisticUpdateConfig<T = any> {
|
|
2
|
+
rollbackOnError?: boolean;
|
|
3
|
+
onRollback?: (originalData: T) => void;
|
|
4
|
+
onCommit?: (newData: T) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface OptimisticUpdateResult<T = any> {
|
|
7
|
+
apply: (newData: T) => string;
|
|
8
|
+
rollback: (updateId: string) => void;
|
|
9
|
+
commit: (updateId: string) => void;
|
|
10
|
+
rollbackAll: () => void;
|
|
11
|
+
getOriginalData: (updateId: string) => T | undefined;
|
|
12
|
+
hasOptimisticUpdates: boolean;
|
|
13
|
+
optimisticUpdateIds: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function useOptimisticUpdates<T = any>(xpath: string, config?: OptimisticUpdateConfig<T>): OptimisticUpdateResult<T>;
|
|
16
|
+
export declare function useOptimisticList<T = any>(xpath: string, config?: OptimisticUpdateConfig<T[]>): OptimisticUpdateResult<T[]> & {
|
|
17
|
+
addItem: (item: T, position?: number) => string;
|
|
18
|
+
updateItem: (index: number, item: T) => string;
|
|
19
|
+
removeItem: (index: number) => string;
|
|
20
|
+
moveItem: (fromIndex: number, toIndex: number) => string;
|
|
21
|
+
};
|
|
22
|
+
export declare function useOptimisticObject<T extends Record<string, any>>(xpath: string, config?: OptimisticUpdateConfig<T>): OptimisticUpdateResult<T> & {
|
|
23
|
+
updateProperty: <K extends keyof T>(key: K, value: T[K]) => string;
|
|
24
|
+
updateProperties: (updates: Partial<T>) => string;
|
|
25
|
+
removeProperty: <K extends keyof T>(key: K) => string;
|
|
26
|
+
mergeObject: (updates: Partial<T>) => string;
|
|
27
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useXPath(): string;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
export { DataProvider } from './provider';
|
|
5
|
+
export { useXPath } from './hooks/useXPath';
|
|
6
|
+
export { useData, useTargetData, useDataAtXPath } from './hooks/useData';
|
|
7
|
+
export { useNavigate, useNavigation, useHistory } from './hooks/useNavigate';
|
|
8
|
+
export { useDataManipulation } from './hooks/useDataManipulation';
|
|
9
|
+
export { useAsyncData } from './hooks/useAsyncData';
|
|
10
|
+
export { useAsyncMutation } from './hooks/useAsyncMutation';
|
|
11
|
+
export { useAsyncState, useAsyncStates, useGlobalAsyncState } from './hooks/useAsyncState';
|
|
12
|
+
export { useAsyncBatch, useAsyncParallel, useAsyncSequential } from './hooks/useAsyncBatch';
|
|
13
|
+
export { useOptimisticUpdates, useOptimisticList, useOptimisticObject } from './hooks/useOptimisticUpdates';
|
|
14
|
+
export { useInvalidation, useAutoInvalidation, useQueryInvalidation } from './hooks/useInvalidation';
|
|
15
|
+
export { asyncStart, asyncSuccess, asyncError, asyncCancel, commandQueueUpdate, generateRequestId } from './state/actions';
|
|
16
|
+
export { CommandQueueManager } from './state/command-queue';
|
|
17
|
+
export type { AsyncState, AsyncCommand, CommandQueue, OptimisticUpdate } from './types';
|
|
18
|
+
export type { AsyncDataConfig, AsyncDataResult } from './hooks/useAsyncData';
|
|
19
|
+
export type { AsyncMutationConfig, AsyncMutationResult } from './hooks/useAsyncMutation';
|
|
20
|
+
export type { AsyncStateInfo, AsyncStateResult } from './hooks/useAsyncState';
|
|
21
|
+
export type { BatchOperation, BatchConfig, BatchResult, AsyncBatchResult } from './hooks/useAsyncBatch';
|
|
22
|
+
export type { OptimisticUpdateConfig, OptimisticUpdateResult } from './hooks/useOptimisticUpdates';
|
|
23
|
+
export type { InvalidationConfig, InvalidationResult } from './hooks/useInvalidation';
|
|
24
|
+
export { Form, Button } from './FormHandler';
|
|
25
|
+
interface LinkProps extends Omit<ComponentProps<'a'>, 'href' | 'onClick'> {
|
|
26
|
+
to: string;
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
export declare const Link: React.ForwardRefExoticComponent<Omit<LinkProps, "ref"> & React.RefAttributes<HTMLAnchorElement>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index'
|