@miurajs/miura-data-flow 0.0.1
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 +565 -0
- package/index.ts +1 -0
- package/package.json +30 -0
- package/src/global-state.ts +158 -0
- package/src/middleware.ts +162 -0
- package/src/miura-data-flow.ts +114 -0
- package/src/providers/firebase-provider.ts +39 -0
- package/src/providers/graphql-provider.ts +39 -0
- package/src/providers/grpc-web-provider.ts +35 -0
- package/src/providers/index.ts +11 -0
- package/src/providers/indexed-db-provider.ts +48 -0
- package/src/providers/local-storage-provider.ts +36 -0
- package/src/providers/provider-manager.ts +21 -0
- package/src/providers/provider.ts +23 -0
- package/src/providers/rest-provider.ts +351 -0
- package/src/providers/s3-provider.ts +41 -0
- package/src/providers/supabase-provider.ts +45 -0
- package/src/providers/websockets-provider.ts +54 -0
- package/src/store.ts +237 -0
- package/stories/data-flow-demo.stories.ts +640 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Store, StoreState, StoreActions } from './store';
|
|
2
|
+
import { debugLog } from '@miura/miura-debugger';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Global state interface
|
|
6
|
+
*/
|
|
7
|
+
export interface GlobalState extends StoreState {
|
|
8
|
+
// Add common global properties
|
|
9
|
+
user?: {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
email: string;
|
|
13
|
+
};
|
|
14
|
+
theme?: 'light' | 'dark';
|
|
15
|
+
language?: string;
|
|
16
|
+
notifications?: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
message: string;
|
|
19
|
+
type: 'info' | 'success' | 'warning' | 'error';
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Global State Manager
|
|
25
|
+
* Manages application-wide state using the Store system
|
|
26
|
+
*/
|
|
27
|
+
export class GlobalStateManager {
|
|
28
|
+
private static instance: GlobalStateManager;
|
|
29
|
+
private store: Store<GlobalState>;
|
|
30
|
+
private componentSubscriptions = new Map<string, Set<string>>();
|
|
31
|
+
|
|
32
|
+
private constructor() {
|
|
33
|
+
this.store = new Store<GlobalState>({
|
|
34
|
+
theme: 'light',
|
|
35
|
+
language: 'en',
|
|
36
|
+
notifications: []
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static getInstance(): GlobalStateManager {
|
|
41
|
+
if (!GlobalStateManager.instance) {
|
|
42
|
+
GlobalStateManager.instance = new GlobalStateManager();
|
|
43
|
+
}
|
|
44
|
+
return GlobalStateManager.instance;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the global store
|
|
49
|
+
*/
|
|
50
|
+
getStore(): Store<GlobalState> {
|
|
51
|
+
return this.store;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get a global property
|
|
56
|
+
*/
|
|
57
|
+
get<K extends keyof GlobalState>(key: K): GlobalState[K] {
|
|
58
|
+
return this.store.get(key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set a global property
|
|
63
|
+
*/
|
|
64
|
+
set<K extends keyof GlobalState>(key: K, value: GlobalState[K]): void {
|
|
65
|
+
this.store.setState({ [key]: value } as Partial<GlobalState>);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Subscribe to global state changes
|
|
70
|
+
*/
|
|
71
|
+
subscribe(
|
|
72
|
+
componentId: string,
|
|
73
|
+
properties: (keyof GlobalState)[],
|
|
74
|
+
callback: (state: GlobalState, prevState: GlobalState) => void
|
|
75
|
+
): () => void {
|
|
76
|
+
// Track component subscriptions
|
|
77
|
+
if (!this.componentSubscriptions.has(componentId)) {
|
|
78
|
+
this.componentSubscriptions.set(componentId, new Set());
|
|
79
|
+
}
|
|
80
|
+
this.componentSubscriptions.get(componentId)!.add(properties.join(','));
|
|
81
|
+
|
|
82
|
+
debugLog('element', 'Subscribed to global state', { componentId, properties });
|
|
83
|
+
|
|
84
|
+
// Subscribe to store with selector
|
|
85
|
+
return this.store.subscribe(
|
|
86
|
+
callback,
|
|
87
|
+
(state) => {
|
|
88
|
+
const selected: Partial<GlobalState> = {};
|
|
89
|
+
properties.forEach(prop => {
|
|
90
|
+
selected[prop] = state[prop];
|
|
91
|
+
});
|
|
92
|
+
return selected;
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Subscribe to a specific global property
|
|
99
|
+
*/
|
|
100
|
+
subscribeTo<K extends keyof GlobalState>(
|
|
101
|
+
componentId: string,
|
|
102
|
+
key: K,
|
|
103
|
+
callback: (value: GlobalState[K], prevValue: GlobalState[K]) => void
|
|
104
|
+
): () => void {
|
|
105
|
+
return this.subscribe(componentId, [key], (state, prevState) => {
|
|
106
|
+
if (state[key] !== prevState[key]) {
|
|
107
|
+
callback(state[key], prevState[key]);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Unsubscribe component from all global state
|
|
114
|
+
*/
|
|
115
|
+
unsubscribe(componentId: string): void {
|
|
116
|
+
this.componentSubscriptions.delete(componentId);
|
|
117
|
+
debugLog('element', 'Unsubscribed from global state', { componentId });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Define global actions
|
|
122
|
+
*/
|
|
123
|
+
defineActions(actions: StoreActions<GlobalState>): void {
|
|
124
|
+
this.store.defineActions(actions);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Dispatch a global action
|
|
129
|
+
*/
|
|
130
|
+
async dispatch(action: string, ...args: unknown[]): Promise<void> {
|
|
131
|
+
await this.store.dispatch(action, ...args);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Add middleware to global store
|
|
136
|
+
*/
|
|
137
|
+
use(middleware: any): void {
|
|
138
|
+
this.store.use(middleware);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get debug information
|
|
143
|
+
*/
|
|
144
|
+
getDebugInfo() {
|
|
145
|
+
return {
|
|
146
|
+
...this.store.getDebugInfo(),
|
|
147
|
+
componentSubscriptions: Object.fromEntries(
|
|
148
|
+
Array.from(this.componentSubscriptions.entries()).map(([id, props]) => [
|
|
149
|
+
id,
|
|
150
|
+
Array.from(props)
|
|
151
|
+
])
|
|
152
|
+
)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Export singleton instance
|
|
158
|
+
export const globalState = GlobalStateManager.getInstance();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { StoreMiddleware, StoreState } from './store';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Logger middleware for debugging
|
|
5
|
+
*/
|
|
6
|
+
export function createLoggerMiddleware(): StoreMiddleware {
|
|
7
|
+
return {
|
|
8
|
+
name: 'logger',
|
|
9
|
+
before: (action, args, state) => {
|
|
10
|
+
console.group(`🔄 Action: ${action}`);
|
|
11
|
+
console.log('Arguments:', args);
|
|
12
|
+
console.log('Current State:', state);
|
|
13
|
+
},
|
|
14
|
+
after: (action, args, state, result) => {
|
|
15
|
+
console.log('New State:', state);
|
|
16
|
+
console.log('Result:', result);
|
|
17
|
+
console.groupEnd();
|
|
18
|
+
},
|
|
19
|
+
error: (action, args, error) => {
|
|
20
|
+
console.error(`❌ Action Error: ${action}`, error);
|
|
21
|
+
console.groupEnd();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Persistence middleware for localStorage
|
|
28
|
+
*/
|
|
29
|
+
export function createPersistenceMiddleware(keys: string[], storageKey = 'miura-store'): StoreMiddleware {
|
|
30
|
+
return {
|
|
31
|
+
name: 'persistence',
|
|
32
|
+
after: (action, args, state) => {
|
|
33
|
+
// Persist relevant keys after every action
|
|
34
|
+
try {
|
|
35
|
+
const toPersist: Partial<StoreState> = {};
|
|
36
|
+
keys.forEach(key => {
|
|
37
|
+
if (key in state) {
|
|
38
|
+
toPersist[key] = state[key];
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
localStorage.setItem(storageKey, JSON.stringify(toPersist));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn('Failed to persist state:', error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Loads persisted state from localStorage.
|
|
51
|
+
* Call this to get initial state when creating a store:
|
|
52
|
+
* const persisted = loadPersistedState(['user', 'theme']);
|
|
53
|
+
* const store = new Store({ ...defaults, ...persisted });
|
|
54
|
+
*/
|
|
55
|
+
export function loadPersistedState(keys: string[], storageKey = 'miura-store'): Partial<StoreState> {
|
|
56
|
+
try {
|
|
57
|
+
const persisted = localStorage.getItem(storageKey);
|
|
58
|
+
if (persisted) {
|
|
59
|
+
const parsed = JSON.parse(persisted);
|
|
60
|
+
const result: Partial<StoreState> = {};
|
|
61
|
+
keys.forEach(key => {
|
|
62
|
+
if (key in parsed) {
|
|
63
|
+
result[key] = parsed[key];
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn('Failed to load persisted state:', error);
|
|
70
|
+
}
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* API middleware for automatic API calls
|
|
76
|
+
*/
|
|
77
|
+
export function createApiMiddleware(apiConfig: {
|
|
78
|
+
baseURL: string;
|
|
79
|
+
headers?: Record<string, string>;
|
|
80
|
+
timeout?: number;
|
|
81
|
+
}): StoreMiddleware & { fetch: (endpoint: string, options?: { method?: string; data?: unknown }) => Promise<unknown> } {
|
|
82
|
+
const apiFetch = async (endpoint: string, options?: { method?: string; data?: unknown }): Promise<unknown> => {
|
|
83
|
+
const method = options?.method || 'GET';
|
|
84
|
+
const data = options?.data;
|
|
85
|
+
|
|
86
|
+
const response = await fetch(`${apiConfig.baseURL}${endpoint}`, {
|
|
87
|
+
method,
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
...apiConfig.headers
|
|
91
|
+
},
|
|
92
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
93
|
+
signal: apiConfig.timeout ? AbortSignal.timeout(apiConfig.timeout) : undefined
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return response.json();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
name: 'api',
|
|
105
|
+
fetch: apiFetch
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Cache middleware for API responses
|
|
111
|
+
*/
|
|
112
|
+
export function createCacheMiddleware(ttl = 5 * 60 * 1000): StoreMiddleware {
|
|
113
|
+
const cache = new Map<string, { data: unknown; timestamp: number }>();
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
name: 'cache',
|
|
117
|
+
before: async (action, args, state) => {
|
|
118
|
+
if (action.startsWith('api_')) {
|
|
119
|
+
const cacheKey = `${action}_${JSON.stringify(args)}`;
|
|
120
|
+
const cached = cache.get(cacheKey);
|
|
121
|
+
|
|
122
|
+
if (cached && Date.now() - cached.timestamp < ttl) {
|
|
123
|
+
// Return cached data
|
|
124
|
+
const endpoint = action.replace('api_', '');
|
|
125
|
+
(state as any)[`${endpoint}_data`] = cached.data;
|
|
126
|
+
(state as any)[`${endpoint}_loading`] = false;
|
|
127
|
+
(state as any)[`${endpoint}_error`] = null;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
after: (action, args, state) => {
|
|
133
|
+
if (action.startsWith('api_')) {
|
|
134
|
+
const cacheKey = `${action}_${JSON.stringify(args)}`;
|
|
135
|
+
const endpoint = action.replace('api_', '');
|
|
136
|
+
const data = (state as any)[`${endpoint}_data`];
|
|
137
|
+
|
|
138
|
+
if (data) {
|
|
139
|
+
cache.set(cacheKey, { data, timestamp: Date.now() });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* DevTools middleware for Redux DevTools integration
|
|
148
|
+
*/
|
|
149
|
+
export function createDevToolsMiddleware(storeName = 'miuraStore'): StoreMiddleware {
|
|
150
|
+
return {
|
|
151
|
+
name: 'devtools',
|
|
152
|
+
after: (action, args, state) => {
|
|
153
|
+
if (typeof window !== 'undefined' && (window as any).__REDUX_DEVTOOLS_EXTENSION__) {
|
|
154
|
+
(window as any).__REDUX_DEVTOOLS_EXTENSION__.send(
|
|
155
|
+
{ type: action, payload: args },
|
|
156
|
+
state,
|
|
157
|
+
storeName
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// miuraDataFlow main entry point
|
|
2
|
+
|
|
3
|
+
// Core exports
|
|
4
|
+
export { Store, type StoreState, type StoreActions, type StoreMiddleware } from './store';
|
|
5
|
+
export { GlobalStateManager, globalState, type GlobalState } from './global-state';
|
|
6
|
+
|
|
7
|
+
// Middleware exports
|
|
8
|
+
export {
|
|
9
|
+
createLoggerMiddleware,
|
|
10
|
+
createPersistenceMiddleware,
|
|
11
|
+
loadPersistedState,
|
|
12
|
+
createApiMiddleware,
|
|
13
|
+
createCacheMiddleware,
|
|
14
|
+
createDevToolsMiddleware
|
|
15
|
+
} from './middleware';
|
|
16
|
+
|
|
17
|
+
// Provider exports
|
|
18
|
+
export * from './providers';
|
|
19
|
+
|
|
20
|
+
// Import for default export
|
|
21
|
+
import { Store } from './store';
|
|
22
|
+
import { GlobalStateManager, globalState } from './global-state';
|
|
23
|
+
import {
|
|
24
|
+
createLoggerMiddleware,
|
|
25
|
+
createPersistenceMiddleware,
|
|
26
|
+
loadPersistedState,
|
|
27
|
+
createApiMiddleware,
|
|
28
|
+
createCacheMiddleware,
|
|
29
|
+
createDevToolsMiddleware
|
|
30
|
+
} from './middleware';
|
|
31
|
+
import * as providers from './providers';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* miura Data Flow - Modern State Management
|
|
35
|
+
*
|
|
36
|
+
* Features:
|
|
37
|
+
* - Reactive state management with subscriptions
|
|
38
|
+
* - Middleware system for logging, persistence, API calls
|
|
39
|
+
* - Global state management
|
|
40
|
+
* - Built-in caching and API integration
|
|
41
|
+
* - Redux DevTools support
|
|
42
|
+
* - TypeScript support
|
|
43
|
+
* - Extensible provider system for connecting to any data source
|
|
44
|
+
*
|
|
45
|
+
* Usage:
|
|
46
|
+
*
|
|
47
|
+
* // Create a store
|
|
48
|
+
* const store = new Store({ count: 0, user: null });
|
|
49
|
+
*
|
|
50
|
+
* // Define actions
|
|
51
|
+
* store.defineActions({
|
|
52
|
+
* increment: (state) => ({ count: state.count + 1 }),
|
|
53
|
+
* setUser: (state, user) => ({ user })
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* // Add middleware
|
|
57
|
+
* store.use(createLoggerMiddleware());
|
|
58
|
+
* store.use(createPersistenceMiddleware(['user']));
|
|
59
|
+
*
|
|
60
|
+
* // Subscribe to changes
|
|
61
|
+
* const unsubscribe = store.subscribe((state, prevState) => {
|
|
62
|
+
* console.log('State changed:', state);
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // Dispatch actions
|
|
66
|
+
* await store.dispatch('increment');
|
|
67
|
+
* await store.dispatch('setUser', { id: '1', name: 'John' });
|
|
68
|
+
*
|
|
69
|
+
* // Using Data Providers
|
|
70
|
+
* // 1. Register a provider factory (e.g., in your app's entry point)
|
|
71
|
+
* providers.registerProvider('restApi', new providers.RestProviderFactory());
|
|
72
|
+
*
|
|
73
|
+
* // 2. Create an instance of the provider
|
|
74
|
+
* const apiProvider = providers.createProvider('restApi', { baseUrl: 'https://my-api.com' });
|
|
75
|
+
*
|
|
76
|
+
* // 3. Use the provider, for example in custom middleware
|
|
77
|
+
* // const user = await apiProvider.get('123', { endpoint: 'users' });
|
|
78
|
+
*
|
|
79
|
+
* // Global state
|
|
80
|
+
* globalState.set('theme', 'dark');
|
|
81
|
+
* globalState.subscribeTo('my-component', 'theme', (theme) => {
|
|
82
|
+
* console.log('Theme changed:', theme);
|
|
83
|
+
* });
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
// Default export for convenience
|
|
87
|
+
export default {
|
|
88
|
+
Store,
|
|
89
|
+
GlobalStateManager,
|
|
90
|
+
globalState,
|
|
91
|
+
middleware: {
|
|
92
|
+
createLoggerMiddleware,
|
|
93
|
+
createPersistenceMiddleware,
|
|
94
|
+
loadPersistedState,
|
|
95
|
+
createApiMiddleware,
|
|
96
|
+
createCacheMiddleware,
|
|
97
|
+
createDevToolsMiddleware
|
|
98
|
+
},
|
|
99
|
+
providers: {
|
|
100
|
+
registerProvider: providers.registerProvider,
|
|
101
|
+
createProvider: providers.createProvider,
|
|
102
|
+
factories: {
|
|
103
|
+
RestProviderFactory: providers.RestProviderFactory,
|
|
104
|
+
GraphQLProviderFactory: providers.GraphQLProviderFactory,
|
|
105
|
+
S3ProviderFactory: providers.S3ProviderFactory,
|
|
106
|
+
LocalStorageProviderFactory: providers.LocalStorageProviderFactory,
|
|
107
|
+
IndexedDBProviderFactory: providers.IndexedDBProviderFactory,
|
|
108
|
+
WebSocketProviderFactory: providers.WebSocketProviderFactory,
|
|
109
|
+
FirebaseProviderFactory: providers.FirebaseProviderFactory,
|
|
110
|
+
SupabaseProviderFactory: providers.SupabaseProviderFactory,
|
|
111
|
+
GrpcWebProviderFactory: providers.GrpcWebProviderFactory,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DataProvider, ProviderFactory } from './provider';
|
|
2
|
+
// A real implementation would import from the Firebase SDK
|
|
3
|
+
// import { initializeApp } from 'firebase/app';
|
|
4
|
+
// import { getFirestore, doc, getDoc, setDoc } from 'firebase/firestore';
|
|
5
|
+
|
|
6
|
+
export interface FirebaseProviderOptions {
|
|
7
|
+
firebaseConfig: object; // The config object from your Firebase project
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class FirebaseProvider<T> implements DataProvider<T> {
|
|
11
|
+
// private firestore: any;
|
|
12
|
+
|
|
13
|
+
constructor(options: FirebaseProviderOptions) {
|
|
14
|
+
// const app = initializeApp(options.firebaseConfig);
|
|
15
|
+
// this.firestore = getFirestore(app);
|
|
16
|
+
console.log('FirebaseProvider initialized with config:', options.firebaseConfig);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async get(id: string, options: { collection: string }): Promise<T> {
|
|
20
|
+
console.log(`[Firestore] Getting doc "${id}" from collection "${options.collection}"`);
|
|
21
|
+
// const docRef = doc(this.firestore, options.collection, id);
|
|
22
|
+
// const docSnap = await getDoc(docRef);
|
|
23
|
+
// if (!docSnap.exists()) throw new Error('Document not found');
|
|
24
|
+
// return docSnap.data() as T;
|
|
25
|
+
return Promise.resolve({} as T);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async put(id: string, data: T, options: { collection: string }): Promise<T> {
|
|
29
|
+
console.log(`[Firestore] Putting doc "${id}" in collection "${options.collection}"`);
|
|
30
|
+
// await setDoc(doc(this.firestore, options.collection, id), data);
|
|
31
|
+
return Promise.resolve(data);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class FirebaseProviderFactory implements ProviderFactory {
|
|
36
|
+
create<T>(options: FirebaseProviderOptions): DataProvider<T> {
|
|
37
|
+
return new FirebaseProvider<T>(options);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DataProvider, ProviderFactory } from './provider';
|
|
2
|
+
import { request } from 'graphql-request'; // Example dependency
|
|
3
|
+
|
|
4
|
+
export interface GraphQLProviderOptions {
|
|
5
|
+
endpoint: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class GraphQLProvider<T> implements DataProvider<T> {
|
|
10
|
+
private options: GraphQLProviderOptions;
|
|
11
|
+
|
|
12
|
+
constructor(options: GraphQLProviderOptions) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async query(options: { query: string; variables?: any }): Promise<any> {
|
|
17
|
+
return request(
|
|
18
|
+
this.options.endpoint,
|
|
19
|
+
options.query,
|
|
20
|
+
options.variables,
|
|
21
|
+
this.options.headers
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async mutate(options: { mutation: string; variables?: any }): Promise<any> {
|
|
26
|
+
return request(
|
|
27
|
+
this.options.endpoint,
|
|
28
|
+
options.mutation,
|
|
29
|
+
options.variables,
|
|
30
|
+
this.options.headers
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class GraphQLProviderFactory implements ProviderFactory {
|
|
36
|
+
create<T>(options: GraphQLProviderOptions): DataProvider<T> {
|
|
37
|
+
return new GraphQLProvider<T>(options);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DataProvider, ProviderFactory } from './provider';
|
|
2
|
+
// A real implementation would use gRPC-Web libraries
|
|
3
|
+
// import { GrpcWebClientBase } from 'grpc-web';
|
|
4
|
+
|
|
5
|
+
export interface GrpcWebProviderOptions {
|
|
6
|
+
hostname: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// This is highly conceptual as gRPC is strongly-typed and code-generated
|
|
10
|
+
class GrpcWebProvider<T> implements DataProvider<T> {
|
|
11
|
+
// private client: GrpcWebClientBase;
|
|
12
|
+
private hostname: string;
|
|
13
|
+
|
|
14
|
+
constructor(options: GrpcWebProviderOptions) {
|
|
15
|
+
// this.client = new GrpcWebClientBase(options);
|
|
16
|
+
this.hostname = options.hostname;
|
|
17
|
+
console.log('gRPC-Web Provider initialized for host:', options.hostname);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async query(options: any): Promise<T[]> {
|
|
21
|
+
console.log(`[gRPC-Web] Query on "${this.hostname}"`, options);
|
|
22
|
+
return Promise.resolve([]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async mutate(options: any): Promise<T> {
|
|
26
|
+
console.log(`[gRPC-Web] Mutate on "${this.hostname}"`, options);
|
|
27
|
+
return Promise.resolve({} as T);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class GrpcWebProviderFactory implements ProviderFactory {
|
|
32
|
+
create<T>(options: GrpcWebProviderOptions): DataProvider<T> {
|
|
33
|
+
return new GrpcWebProvider<T>(options);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './provider';
|
|
2
|
+
export * from './provider-manager';
|
|
3
|
+
export * from './graphql-provider';
|
|
4
|
+
export * from './s3-provider';
|
|
5
|
+
export * from './local-storage-provider';
|
|
6
|
+
export * from './rest-provider';
|
|
7
|
+
export * from './indexed-db-provider';
|
|
8
|
+
export * from './websockets-provider';
|
|
9
|
+
export * from './firebase-provider';
|
|
10
|
+
export * from './supabase-provider';
|
|
11
|
+
export * from './grpc-web-provider';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { DataProvider, ProviderFactory } from './provider';
|
|
2
|
+
// A real implementation would use a library like 'idb' for a friendlier API
|
|
3
|
+
// import { openDB, IDBPDatabase } from 'idb';
|
|
4
|
+
|
|
5
|
+
export interface IndexedDBProviderOptions {
|
|
6
|
+
dbName: string;
|
|
7
|
+
storeName: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class IndexedDBProvider<T> implements DataProvider<T> {
|
|
11
|
+
private dbPromise: Promise<any>; // Promise<IDBPDatabase>
|
|
12
|
+
|
|
13
|
+
constructor(options: IndexedDBProviderOptions) {
|
|
14
|
+
// This is highly simplified
|
|
15
|
+
this.dbPromise = Promise.resolve(); // openDB(options.dbName, 1, { ... });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async get(id: string): Promise<T> {
|
|
19
|
+
console.log(`[IndexedDB] Getting item with id: ${id}`);
|
|
20
|
+
// const db = await this.dbPromise;
|
|
21
|
+
// const tx = db.transaction(this.storeName, 'readonly');
|
|
22
|
+
// const store = tx.objectStore(this.storeName);
|
|
23
|
+
// const result = await store.get(id);
|
|
24
|
+
// if (!result) throw new Error('Not found');
|
|
25
|
+
// return result;
|
|
26
|
+
return Promise.resolve({} as T);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async put(id: string, data: T): Promise<T> {
|
|
30
|
+
console.log(`[IndexedDB] Putting item with id: ${id}`);
|
|
31
|
+
// const db = await this.dbPromise;
|
|
32
|
+
// const tx = db.transaction(this.storeName, 'readwrite');
|
|
33
|
+
// ...
|
|
34
|
+
return Promise.resolve(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async delete(id: string): Promise<void> {
|
|
38
|
+
console.log(`[IndexedDB] Deleting item with id: ${id}`);
|
|
39
|
+
// ...
|
|
40
|
+
return Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class IndexedDBProviderFactory implements ProviderFactory {
|
|
45
|
+
create<T>(options: IndexedDBProviderOptions): DataProvider<T> {
|
|
46
|
+
return new IndexedDBProvider<T>(options);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DataProvider, ProviderFactory } from './provider';
|
|
2
|
+
|
|
3
|
+
class LocalStorageProvider<T> implements DataProvider<T> {
|
|
4
|
+
private prefix: string;
|
|
5
|
+
|
|
6
|
+
constructor(options: { prefix: string }) {
|
|
7
|
+
this.prefix = options.prefix || 'miura';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
private getKey(id: string): string {
|
|
11
|
+
return `${this.prefix}:${id}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async get(id: string): Promise<T> {
|
|
15
|
+
const item = localStorage.getItem(this.getKey(id));
|
|
16
|
+
if (item === null) {
|
|
17
|
+
throw new Error(`Item with id "${id}" not found in localStorage.`);
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(item);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async put(id: string, data: T): Promise<T> {
|
|
23
|
+
localStorage.setItem(this.getKey(id), JSON.stringify(data));
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async delete(id: string): Promise<void> {
|
|
28
|
+
localStorage.removeItem(this.getKey(id));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class LocalStorageProviderFactory implements ProviderFactory {
|
|
33
|
+
create<T>(options: { prefix: string }): DataProvider<T> {
|
|
34
|
+
return new LocalStorageProvider<T>(options);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DataProvider, ProviderFactory } from './provider';
|
|
2
|
+
|
|
3
|
+
const providerFactories = new Map<string, ProviderFactory>();
|
|
4
|
+
|
|
5
|
+
export const registerProvider = (name: string, factory: ProviderFactory) => {
|
|
6
|
+
if (providerFactories.has(name)) {
|
|
7
|
+
console.warn(`Provider with name "${name}" is already registered.`);
|
|
8
|
+
}
|
|
9
|
+
providerFactories.set(name, factory);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const createProvider = <T>(
|
|
13
|
+
name: string,
|
|
14
|
+
options: any
|
|
15
|
+
): DataProvider<T> | undefined => {
|
|
16
|
+
const factory = providerFactories.get(name);
|
|
17
|
+
if (!factory) {
|
|
18
|
+
throw new Error(`No provider registered with name "${name}"`);
|
|
19
|
+
}
|
|
20
|
+
return factory.create<T>(options);
|
|
21
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface DataProvider<T> {
|
|
2
|
+
// Queries data, for sources like GraphQL/REST
|
|
3
|
+
query?(options: any): Promise<T[]>;
|
|
4
|
+
|
|
5
|
+
// Mutates data, for sources like GraphQL/REST
|
|
6
|
+
mutate?(options: any): Promise<T>;
|
|
7
|
+
|
|
8
|
+
// Retrieves an object, for sources like S3
|
|
9
|
+
get?(id: string, options?: any): Promise<T>;
|
|
10
|
+
|
|
11
|
+
// Puts an object, for sources like S3
|
|
12
|
+
put?(id: string, data: T, options?: any): Promise<T>;
|
|
13
|
+
|
|
14
|
+
// Deletes an object
|
|
15
|
+
delete?(id: string, options?: any): Promise<void>;
|
|
16
|
+
|
|
17
|
+
// Subscribes to real-time updates if supported
|
|
18
|
+
subscribe?(options: any, callback: (data: T) => void): () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProviderFactory {
|
|
22
|
+
create<T>(options: any): DataProvider<T>;
|
|
23
|
+
}
|