@miurajs/miura-data-flow 0.0.3 → 0.1.2

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.
Files changed (84) hide show
  1. package/README.md +15 -15
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +2 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/src/global-state.d.ts +79 -0
  7. package/dist/src/global-state.d.ts.map +1 -0
  8. package/dist/src/global-state.js +111 -0
  9. package/dist/src/global-state.js.map +1 -0
  10. package/dist/src/middleware.d.ts +38 -0
  11. package/dist/src/middleware.d.ts.map +1 -0
  12. package/dist/src/middleware.js +143 -0
  13. package/dist/src/middleware.js.map +1 -0
  14. package/{src/miura-data-flow.ts → dist/src/miura-data-flow.d.ts} +38 -67
  15. package/dist/src/miura-data-flow.d.ts.map +1 -0
  16. package/dist/src/miura-data-flow.js +96 -0
  17. package/dist/src/miura-data-flow.js.map +1 -0
  18. package/dist/src/providers/firebase-provider.d.ts +8 -0
  19. package/dist/src/providers/firebase-provider.d.ts.map +1 -0
  20. package/dist/src/providers/firebase-provider.js +27 -0
  21. package/dist/src/providers/firebase-provider.js.map +1 -0
  22. package/dist/src/providers/graphql-provider.d.ts +9 -0
  23. package/dist/src/providers/graphql-provider.d.ts.map +1 -0
  24. package/dist/src/providers/graphql-provider.js +19 -0
  25. package/dist/src/providers/graphql-provider.js.map +1 -0
  26. package/dist/src/providers/grpc-web-provider.d.ts +8 -0
  27. package/dist/src/providers/grpc-web-provider.d.ts.map +1 -0
  28. package/dist/src/providers/grpc-web-provider.js +24 -0
  29. package/dist/src/providers/grpc-web-provider.js.map +1 -0
  30. package/{src/providers/index.ts → dist/src/providers/index.d.ts} +1 -5
  31. package/dist/src/providers/index.d.ts.map +1 -0
  32. package/dist/src/providers/index.js +11 -0
  33. package/dist/src/providers/index.js.map +1 -0
  34. package/dist/src/providers/indexed-db-provider.d.ts +9 -0
  35. package/dist/src/providers/indexed-db-provider.d.ts.map +1 -0
  36. package/dist/src/providers/indexed-db-provider.js +35 -0
  37. package/dist/src/providers/indexed-db-provider.js.map +1 -0
  38. package/dist/src/providers/local-storage-provider.d.ts +7 -0
  39. package/dist/src/providers/local-storage-provider.d.ts.map +1 -0
  40. package/dist/src/providers/local-storage-provider.js +29 -0
  41. package/dist/src/providers/local-storage-provider.js.map +1 -0
  42. package/dist/src/providers/provider-manager.d.ts +4 -0
  43. package/dist/src/providers/provider-manager.d.ts.map +1 -0
  44. package/dist/src/providers/provider-manager.js +15 -0
  45. package/dist/src/providers/provider-manager.js.map +1 -0
  46. package/dist/src/providers/provider.d.ts +12 -0
  47. package/dist/src/providers/provider.d.ts.map +1 -0
  48. package/dist/src/providers/provider.js +2 -0
  49. package/dist/src/providers/provider.js.map +1 -0
  50. package/dist/src/providers/rest-provider.d.ts +86 -0
  51. package/dist/src/providers/rest-provider.d.ts.map +1 -0
  52. package/dist/src/providers/rest-provider.js +210 -0
  53. package/dist/src/providers/rest-provider.js.map +1 -0
  54. package/dist/src/providers/s3-provider.d.ts +9 -0
  55. package/dist/src/providers/s3-provider.d.ts.map +1 -0
  56. package/dist/src/providers/s3-provider.js +31 -0
  57. package/dist/src/providers/s3-provider.js.map +1 -0
  58. package/dist/src/providers/supabase-provider.d.ts +9 -0
  59. package/dist/src/providers/supabase-provider.d.ts.map +1 -0
  60. package/dist/src/providers/supabase-provider.js +33 -0
  61. package/dist/src/providers/supabase-provider.js.map +1 -0
  62. package/dist/src/providers/websockets-provider.d.ts +9 -0
  63. package/dist/src/providers/websockets-provider.d.ts.map +1 -0
  64. package/dist/src/providers/websockets-provider.js +43 -0
  65. package/dist/src/providers/websockets-provider.js.map +1 -0
  66. package/dist/src/store.d.ts +94 -0
  67. package/dist/src/store.d.ts.map +1 -0
  68. package/dist/src/store.js +169 -0
  69. package/dist/src/store.js.map +1 -0
  70. package/dist/tsconfig.tsbuildinfo +1 -0
  71. package/package.json +34 -7
  72. package/src/providers/graphql-provider.ts +13 -7
  73. package/index.ts +0 -1
  74. package/src/global-state.ts +0 -158
  75. package/src/middleware.ts +0 -162
  76. package/src/providers/indexed-db-provider.ts +0 -48
  77. package/src/providers/local-storage-provider.ts +0 -36
  78. package/src/providers/provider-manager.ts +0 -21
  79. package/src/providers/provider.ts +0 -23
  80. package/src/providers/rest-provider.ts +0 -351
  81. package/src/providers/websockets-provider.ts +0 -54
  82. package/src/store.ts +0 -237
  83. package/stories/data-flow-demo.stories.ts +0 -640
  84. package/tsconfig.json +0 -17
package/src/middleware.ts DELETED
@@ -1,162 +0,0 @@
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
- }
@@ -1,48 +0,0 @@
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
- }
@@ -1,36 +0,0 @@
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
- }
@@ -1,21 +0,0 @@
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
- };
@@ -1,23 +0,0 @@
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
- }
@@ -1,351 +0,0 @@
1
- import { DataProvider, ProviderFactory } from './provider';
2
-
3
- // ── Error type ────────────────────────────────────────────────────────────────
4
-
5
- export class RestError extends Error {
6
- constructor(
7
- public readonly status: number,
8
- public readonly statusText: string,
9
- public readonly body: unknown,
10
- public readonly url: string,
11
- ) {
12
- super(`[${status}] ${statusText} — ${url}`);
13
- this.name = 'RestError';
14
- }
15
- }
16
-
17
- // ── Interceptors ──────────────────────────────────────────────────────────────
18
-
19
- export type RequestInterceptor = (
20
- url: string,
21
- init: RequestInit,
22
- ) => RequestInit | Promise<RequestInit>;
23
-
24
- export type ResponseInterceptor = (
25
- response: Response,
26
- url: string,
27
- ) => Response | Promise<Response>;
28
-
29
- // ── Pagination ────────────────────────────────────────────────────────────────
30
-
31
- export interface PaginationConfig {
32
- /** Strategy for adding pagination params to the request */
33
- strategy: 'query' | 'header' | 'cursor';
34
- /** Query param name for page number (query strategy) */
35
- pageParam?: string;
36
- /** Query param name for page size (query strategy) */
37
- pageSizeParam?: string;
38
- /** Default page size */
39
- pageSize?: number;
40
- /** Extract total count from response (used to determine if more pages exist) */
41
- totalExtractor?: (response: Response, body: unknown) => number;
42
- /** Extract next cursor from response body (cursor strategy) */
43
- cursorExtractor?: (body: unknown) => string | null;
44
- }
45
-
46
- export interface PageResult<T> {
47
- data: T[];
48
- total: number;
49
- page: number;
50
- pageSize: number;
51
- hasMore: boolean;
52
- nextCursor?: string | null;
53
- }
54
-
55
- // ── Cache ─────────────────────────────────────────────────────────────────────
56
-
57
- interface CacheEntry<T> {
58
- data: T;
59
- expiresAt: number;
60
- }
61
-
62
- // ── Provider options ──────────────────────────────────────────────────────────
63
-
64
- export interface RetryConfig {
65
- /** Maximum number of retry attempts (default: 3) */
66
- maxAttempts: number;
67
- /** Initial delay in ms before first retry (default: 300) */
68
- initialDelay: number;
69
- /** Multiplier applied to delay on each subsequent attempt (default: 2) */
70
- backoffFactor: number;
71
- /** HTTP status codes that should trigger a retry (default: [429, 500, 502, 503, 504]) */
72
- retryOn: number[];
73
- }
74
-
75
- export interface RestProviderOptions {
76
- /** Base URL — path segments are appended to this */
77
- baseUrl: string;
78
- /** Default headers merged into every request */
79
- headers?: Record<string, string>;
80
- /** Request timeout in ms (default: 30_000) */
81
- timeout?: number;
82
- /** Retry configuration */
83
- retry?: Partial<RetryConfig>;
84
- /** Response cache TTL in ms — set to 0 to disable (default: 0) */
85
- cacheTtl?: number;
86
- /** Pagination defaults */
87
- pagination?: PaginationConfig;
88
- /** Called before every request — use to inject auth headers, sign requests, etc. */
89
- requestInterceptors?: RequestInterceptor[];
90
- /** Called after every successful response — use to unwrap envelopes, normalise shapes, etc. */
91
- responseInterceptors?: ResponseInterceptor[];
92
- }
93
-
94
- // ── Query/mutation option shapes ──────────────────────────────────────────────
95
-
96
- export interface QueryOptions {
97
- endpoint: string;
98
- params?: Record<string, string | number | boolean>;
99
- headers?: Record<string, string>;
100
- signal?: AbortSignal;
101
- }
102
-
103
- export interface GetOptions {
104
- endpoint: string;
105
- headers?: Record<string, string>;
106
- signal?: AbortSignal;
107
- }
108
-
109
- export interface MutateOptions<T> {
110
- endpoint: string;
111
- data: T;
112
- method?: 'POST' | 'PUT' | 'PATCH';
113
- headers?: Record<string, string>;
114
- signal?: AbortSignal;
115
- }
116
-
117
- export interface PaginatedQueryOptions extends QueryOptions {
118
- page?: number;
119
- cursor?: string;
120
- }
121
-
122
- // ── Implementation ────────────────────────────────────────────────────────────
123
-
124
- class RestProvider<T> implements DataProvider<T> {
125
- private readonly retryConfig: RetryConfig;
126
- private readonly cache = new Map<string, CacheEntry<unknown>>();
127
-
128
- constructor(private readonly options: RestProviderOptions) {
129
- this.retryConfig = {
130
- maxAttempts: 3,
131
- initialDelay: 300,
132
- backoffFactor: 2,
133
- retryOn: [429, 500, 502, 503, 504],
134
- ...options.retry,
135
- };
136
- }
137
-
138
- // ── Public API ─────────────────────────────────────────────────
139
-
140
- async query(options: QueryOptions): Promise<T[]> {
141
- const url = this.buildUrl(options.endpoint, options.params);
142
- const cached = this.getCache<T[]>(url);
143
- if (cached) return cached;
144
-
145
- const response = await this.request(url, { method: 'GET', headers: this.mergeHeaders(options.headers) }, options.signal);
146
- const data: T[] = await response.json();
147
- this.setCache(url, data);
148
- return data;
149
- }
150
-
151
- async get(id: string, options: GetOptions): Promise<T> {
152
- const url = this.buildUrl(`${options.endpoint}/${id}`);
153
- const cached = this.getCache<T>(url);
154
- if (cached) return cached;
155
-
156
- const response = await this.request(url, { method: 'GET', headers: this.mergeHeaders(options.headers) }, options.signal);
157
- const data: T = await response.json();
158
- this.setCache(url, data);
159
- return data;
160
- }
161
-
162
- async mutate(options: MutateOptions<T>): Promise<T> {
163
- const url = this.buildUrl(options.endpoint);
164
- this.invalidateCachePrefix(options.endpoint);
165
-
166
- const response = await this.request(
167
- url,
168
- {
169
- method: options.method ?? 'POST',
170
- headers: this.mergeHeaders(options.headers),
171
- body: JSON.stringify(options.data),
172
- },
173
- options.signal,
174
- );
175
- return response.json();
176
- }
177
-
178
- async put(id: string, data: T, options: GetOptions): Promise<T> {
179
- return this.mutate({ endpoint: `${options.endpoint}/${id}`, data, method: 'PUT', headers: options.headers, signal: options.signal });
180
- }
181
-
182
- async delete(id: string, options: GetOptions): Promise<void> {
183
- const url = this.buildUrl(`${options.endpoint}/${id}`);
184
- this.invalidateCachePrefix(options.endpoint);
185
- await this.request(url, { method: 'DELETE', headers: this.mergeHeaders(options.headers) }, options.signal);
186
- }
187
-
188
- async queryPage(options: PaginatedQueryOptions): Promise<PageResult<T>> {
189
- const pagination = this.options.pagination;
190
- const params: Record<string, string | number | boolean> = { ...options.params };
191
-
192
- if (pagination?.strategy === 'query') {
193
- const pageParam = pagination.pageParam ?? 'page';
194
- const sizeParam = pagination.pageSizeParam ?? 'pageSize';
195
- params[pageParam] = options.page ?? 1;
196
- params[sizeParam] = pagination.pageSize ?? 20;
197
- } else if (pagination?.strategy === 'cursor' && options.cursor) {
198
- params['cursor'] = options.cursor;
199
- }
200
-
201
- const url = this.buildUrl(options.endpoint, params);
202
- const response = await this.request(url, { method: 'GET', headers: this.mergeHeaders(options.headers) }, options.signal);
203
- const body: T[] | { data: T[]; [k: string]: unknown } = await response.json();
204
-
205
- const data: T[] = Array.isArray(body) ? body : (body.data as T[]);
206
- const total = pagination?.totalExtractor?.(response, body) ?? data.length;
207
- const pageSize = (pagination?.pageSize ?? 20);
208
- const page = options.page ?? 1;
209
- const nextCursor = pagination?.cursorExtractor?.(body) ?? null;
210
-
211
- return {
212
- data,
213
- total,
214
- page,
215
- pageSize,
216
- hasMore: nextCursor != null || page * pageSize < total,
217
- nextCursor,
218
- };
219
- }
220
-
221
- // ── Core fetch with interceptors, retry, and timeout ───────────
222
-
223
- private async request(
224
- url: string,
225
- init: RequestInit,
226
- signal?: AbortSignal,
227
- ): Promise<Response> {
228
- let resolvedInit = await this.applyRequestInterceptors(url, init);
229
-
230
- const timeoutMs = this.options.timeout ?? 30_000;
231
- const attempt = async (attemptsLeft: number, delay: number): Promise<Response> => {
232
- const controller = new AbortController();
233
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
234
-
235
- // Merge caller signal with our timeout signal
236
- const combinedSignal = signal
237
- ? this.mergeSignals(signal, controller.signal)
238
- : controller.signal;
239
-
240
- try {
241
- const response = await fetch(url, { ...resolvedInit, signal: combinedSignal });
242
- clearTimeout(timeoutId);
243
-
244
- if (!response.ok && this.retryConfig.retryOn.includes(response.status) && attemptsLeft > 0) {
245
- await this.sleep(delay);
246
- return attempt(attemptsLeft - 1, delay * this.retryConfig.backoffFactor);
247
- }
248
-
249
- if (!response.ok) {
250
- const body = await response.json().catch(() => null);
251
- throw new RestError(response.status, response.statusText, body, url);
252
- }
253
-
254
- return this.applyResponseInterceptors(response, url);
255
- } catch (err) {
256
- clearTimeout(timeoutId);
257
- if (err instanceof RestError) throw err;
258
- // Network / abort error — retry if attempts remain
259
- if (attemptsLeft > 0 && !(signal?.aborted)) {
260
- await this.sleep(delay);
261
- return attempt(attemptsLeft - 1, delay * this.retryConfig.backoffFactor);
262
- }
263
- throw err;
264
- }
265
- };
266
-
267
- return attempt(this.retryConfig.maxAttempts - 1, this.retryConfig.initialDelay);
268
- }
269
-
270
- private async applyRequestInterceptors(url: string, init: RequestInit): Promise<RequestInit> {
271
- let result = init;
272
- for (const interceptor of this.options.requestInterceptors ?? []) {
273
- result = await interceptor(url, result);
274
- }
275
- return result;
276
- }
277
-
278
- private async applyResponseInterceptors(response: Response, url: string): Promise<Response> {
279
- let result = response;
280
- for (const interceptor of this.options.responseInterceptors ?? []) {
281
- result = await interceptor(result, url);
282
- }
283
- return result;
284
- }
285
-
286
- // ── Helpers ────────────────────────────────────────────────────
287
-
288
- private buildUrl(path: string, params?: Record<string, string | number | boolean>): string {
289
- const base = this.options.baseUrl.replace(/\/$/, '');
290
- const normalised = path.startsWith('/') ? path : `/${path}`;
291
- const url = `${base}${normalised}`;
292
- if (!params || Object.keys(params).length === 0) return url;
293
- const qs = new URLSearchParams(
294
- Object.entries(params).map(([k, v]) => [k, String(v)]),
295
- ).toString();
296
- return `${url}?${qs}`;
297
- }
298
-
299
- private mergeHeaders(extra?: Record<string, string>): Record<string, string> {
300
- return {
301
- 'Content-Type': 'application/json',
302
- ...this.options.headers,
303
- ...extra,
304
- };
305
- }
306
-
307
- private mergeSignals(a: AbortSignal, b: AbortSignal): AbortSignal {
308
- const controller = new AbortController();
309
- const abort = () => controller.abort();
310
- a.addEventListener('abort', abort, { once: true });
311
- b.addEventListener('abort', abort, { once: true });
312
- return controller.signal;
313
- }
314
-
315
- private sleep(ms: number): Promise<void> {
316
- return new Promise(resolve => setTimeout(resolve, ms));
317
- }
318
-
319
- // ── Cache helpers ──────────────────────────────────────────────
320
-
321
- private getCache<R>(key: string): R | null {
322
- if (!this.options.cacheTtl) return null;
323
- const entry = this.cache.get(key) as CacheEntry<R> | undefined;
324
- if (!entry) return null;
325
- if (Date.now() > entry.expiresAt) {
326
- this.cache.delete(key);
327
- return null;
328
- }
329
- return entry.data;
330
- }
331
-
332
- private setCache(key: string, data: unknown): void {
333
- if (!this.options.cacheTtl) return;
334
- this.cache.set(key, { data, expiresAt: Date.now() + this.options.cacheTtl });
335
- }
336
-
337
- private invalidateCachePrefix(prefix: string): void {
338
- const normalised = this.buildUrl(prefix);
339
- for (const key of this.cache.keys()) {
340
- if (key.startsWith(normalised)) this.cache.delete(key);
341
- }
342
- }
343
- }
344
-
345
- // ── Factory ───────────────────────────────────────────────────────────────────
346
-
347
- export class RestProviderFactory implements ProviderFactory {
348
- create<T>(options: RestProviderOptions): DataProvider<T> {
349
- return new RestProvider<T>(options);
350
- }
351
- }
@@ -1,54 +0,0 @@
1
- import { DataProvider, ProviderFactory } from './provider';
2
-
3
- export interface WebSocketProviderOptions {
4
- url: string;
5
- protocols?: string | string[];
6
- }
7
-
8
- class WebSocketProvider<T> implements DataProvider<T> {
9
- private ws: WebSocket;
10
-
11
- constructor(options: WebSocketProviderOptions) {
12
- this.ws = new WebSocket(options.url, options.protocols);
13
- }
14
-
15
- subscribe(options: { event: string }, callback: (data: T) => void): () => void {
16
- const handler = (event: MessageEvent) => {
17
- try {
18
- const message = JSON.parse(event.data);
19
- // Assuming a message format of { event: 'name', payload: ... }
20
- if (message.event === options.event) {
21
- callback(message.payload);
22
- }
23
- } catch (e) {
24
- console.error('Error parsing WebSocket message', e);
25
- }
26
- };
27
-
28
- this.ws.addEventListener('message', handler);
29
-
30
- // Return an unsubscribe function
31
- return () => {
32
- this.ws.removeEventListener('message', handler);
33
- };
34
- }
35
-
36
- // A method to send data back to the server
37
- send(event: string, payload: any) {
38
- if (this.ws.readyState === WebSocket.OPEN) {
39
- this.ws.send(JSON.stringify({ event, payload }));
40
- } else {
41
- console.warn('WebSocket is not open. Cannot send message.');
42
- }
43
- }
44
-
45
- close() {
46
- this.ws.close();
47
- }
48
- }
49
-
50
- export class WebSocketProviderFactory implements ProviderFactory {
51
- create<T>(options: WebSocketProviderOptions): DataProvider<T> {
52
- return new WebSocketProvider<T>(options);
53
- }
54
- }