@miurajs/miura-data-flow 0.0.3 → 0.1.4

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 (64) hide show
  1. package/README.md +187 -16
  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/{src/providers/index.ts → dist/src/providers/index.d.ts} +1 -5
  19. package/dist/src/providers/index.d.ts.map +1 -0
  20. package/dist/src/providers/index.js +11 -0
  21. package/dist/src/providers/index.js.map +1 -0
  22. package/dist/src/providers/indexed-db-provider.d.ts +9 -0
  23. package/dist/src/providers/indexed-db-provider.d.ts.map +1 -0
  24. package/dist/src/providers/indexed-db-provider.js +35 -0
  25. package/dist/src/providers/indexed-db-provider.js.map +1 -0
  26. package/dist/src/providers/local-storage-provider.d.ts +7 -0
  27. package/dist/src/providers/local-storage-provider.d.ts.map +1 -0
  28. package/dist/src/providers/local-storage-provider.js +29 -0
  29. package/dist/src/providers/local-storage-provider.js.map +1 -0
  30. package/dist/src/providers/provider-manager.d.ts +4 -0
  31. package/dist/src/providers/provider-manager.d.ts.map +1 -0
  32. package/dist/src/providers/provider-manager.js +15 -0
  33. package/dist/src/providers/provider-manager.js.map +1 -0
  34. package/dist/src/providers/provider.d.ts +12 -0
  35. package/dist/src/providers/provider.d.ts.map +1 -0
  36. package/dist/src/providers/provider.js +2 -0
  37. package/dist/src/providers/provider.js.map +1 -0
  38. package/dist/src/providers/rest-provider.d.ts +86 -0
  39. package/dist/src/providers/rest-provider.d.ts.map +1 -0
  40. package/dist/src/providers/rest-provider.js +210 -0
  41. package/dist/src/providers/rest-provider.js.map +1 -0
  42. package/dist/src/providers/websockets-provider.d.ts +9 -0
  43. package/dist/src/providers/websockets-provider.d.ts.map +1 -0
  44. package/dist/src/providers/websockets-provider.js +43 -0
  45. package/dist/src/providers/websockets-provider.js.map +1 -0
  46. package/dist/src/store.d.ts +94 -0
  47. package/dist/src/store.d.ts.map +1 -0
  48. package/dist/src/store.js +169 -0
  49. package/dist/src/store.js.map +1 -0
  50. package/dist/tsconfig.tsbuildinfo +1 -0
  51. package/package.json +34 -7
  52. package/src/providers/graphql-provider.ts +13 -7
  53. package/index.ts +0 -1
  54. package/src/global-state.ts +0 -158
  55. package/src/middleware.ts +0 -162
  56. package/src/providers/indexed-db-provider.ts +0 -48
  57. package/src/providers/local-storage-provider.ts +0 -36
  58. package/src/providers/provider-manager.ts +0 -21
  59. package/src/providers/provider.ts +0 -23
  60. package/src/providers/rest-provider.ts +0 -351
  61. package/src/providers/websockets-provider.ts +0 -54
  62. package/src/store.ts +0 -237
  63. package/stories/data-flow-demo.stories.ts +0 -640
  64. package/tsconfig.json +0 -17
@@ -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
- }
package/src/store.ts DELETED
@@ -1,237 +0,0 @@
1
- import { debugLog } from '@miura/miura-debugger';
2
-
3
- /**
4
- * Store state interface
5
- */
6
- export interface StoreState {
7
- [key: string]: unknown;
8
- }
9
-
10
- /**
11
- * Store actions interface
12
- */
13
- export type StoreAction<T extends StoreState> = (
14
- state: T,
15
- ...args: any[]
16
- ) => Partial<T> | Promise<Partial<T>>;
17
-
18
- export interface StoreActions<T extends StoreState> {
19
- [key: string]: StoreAction<T>;
20
- }
21
-
22
- /**
23
- * Store middleware interface
24
- */
25
- export interface StoreMiddleware {
26
- name: string;
27
- before?: (action: string, args: unknown[], state: StoreState) => void | Promise<void>;
28
- after?: (action: string, args: unknown[], state: StoreState, result: unknown) => void | Promise<void>;
29
- error?: (action: string, args: unknown[], error: Error) => void | Promise<void>;
30
- }
31
-
32
- /**
33
- * Store subscriber
34
- */
35
- export interface StoreSubscriber {
36
- id: string;
37
- selector?: (state: StoreState) => unknown;
38
- callback: (state: StoreState, prevState: StoreState) => void;
39
- }
40
-
41
- /**
42
- * Modern Store Implementation
43
- * Combines the best of Redux, Zustand, and modern patterns
44
- */
45
- export class Store<T extends StoreState = StoreState> {
46
- private state: T;
47
- private actions: StoreActions<T> = {};
48
- private subscribers = new Map<string, StoreSubscriber>();
49
- private middlewares: StoreMiddleware[] = [];
50
- private isUpdating = false;
51
- private updateQueue: Array<() => void> = [];
52
- private subscriberIdCounter = 0;
53
-
54
- constructor(initialState: T) {
55
- this.state = { ...initialState };
56
- }
57
-
58
- /**
59
- * Get current state
60
- */
61
- getState(): T {
62
- return { ...this.state };
63
- }
64
-
65
- /**
66
- * Get a specific property from state
67
- */
68
- get<K extends keyof T>(key: K): T[K] {
69
- return this.state[key];
70
- }
71
-
72
- /**
73
- * Set state (immutable update)
74
- */
75
- setState(updater: Partial<T> | ((state: T) => Partial<T>)): void {
76
- const newState = typeof updater === 'function' ? updater(this.state) : updater;
77
-
78
- this.updateState(newState as Partial<T>);
79
- }
80
-
81
- /**
82
- * Define actions (like Redux actions but simpler)
83
- */
84
- defineActions(actions: StoreActions<T>): void {
85
- this.actions = { ...this.actions, ...actions };
86
- }
87
-
88
- /**
89
- * Execute an action
90
- */
91
- async dispatch(action: string, ...args: unknown[]): Promise<void> {
92
- if (!this.actions[action]) {
93
- throw new Error(`Action '${action}' not found`);
94
- }
95
-
96
- try {
97
- // Run before middlewares
98
- for (const middleware of this.middlewares) {
99
- if (middleware.before) {
100
- await middleware.before(action, args, this.state);
101
- }
102
- }
103
-
104
- // Execute action and update state
105
- const result = await this.actions[action](this.state, ...args);
106
- this.updateState(result as Partial<T>);
107
-
108
- // Run after middlewares
109
- for (const middleware of this.middlewares) {
110
- if (middleware.after) {
111
- await middleware.after(action, args, this.state, result);
112
- }
113
- }
114
- } catch (error) {
115
- // Run error middlewares
116
- for (const middleware of this.middlewares) {
117
- if (middleware.error) {
118
- await middleware.error(action, args, error as Error);
119
- }
120
- }
121
- throw error;
122
- }
123
- }
124
-
125
- /**
126
- * Subscribe to state changes
127
- */
128
- subscribe(
129
- callback: (state: T, prevState: T) => void,
130
- selector?: (state: T) => unknown
131
- ): () => void {
132
- const id = `subscriber_${++this.subscriberIdCounter}`;
133
- const subscriber: StoreSubscriber = {
134
- id,
135
- selector: selector as (state: StoreState) => unknown,
136
- callback: callback as (state: StoreState, prevState: StoreState) => void
137
- };
138
-
139
- this.subscribers.set(id, subscriber);
140
- debugLog('element', 'Subscribed to store', { id, hasSelector: !!selector });
141
-
142
- // Return unsubscribe function
143
- return () => {
144
- this.subscribers.delete(id);
145
- debugLog('element', 'Unsubscribed from store', { id });
146
- };
147
- }
148
-
149
- /**
150
- * Subscribe to specific property changes
151
- */
152
- subscribeTo<K extends keyof T>(
153
- key: K,
154
- callback: (value: T[K], prevValue: T[K]) => void
155
- ): () => void {
156
- return this.subscribe(
157
- (state, prevState) => {
158
- if (state[key] !== prevState[key]) {
159
- callback(state[key], prevState[key]);
160
- }
161
- },
162
- (state) => state[key]
163
- );
164
- }
165
-
166
- /**
167
- * Add middleware
168
- */
169
- use(middleware: StoreMiddleware): void {
170
- this.middlewares.push(middleware);
171
- debugLog('element', 'Added middleware', { name: middleware.name });
172
- }
173
-
174
- /**
175
- * Update state and notify subscribers
176
- */
177
- private updateState(updater: Partial<T>): void {
178
- if (this.isUpdating) {
179
- // Queue update if already updating
180
- this.updateQueue.push(() => this.updateState(updater));
181
- return;
182
- }
183
-
184
- this.isUpdating = true;
185
- const prevState = { ...this.state };
186
-
187
- // Apply updates
188
- this.state = { ...this.state, ...updater };
189
-
190
- // Notify subscribers
191
- this.notifySubscribers(prevState);
192
-
193
- this.isUpdating = false;
194
-
195
- // Process queued updates
196
- while (this.updateQueue.length > 0) {
197
- const queuedUpdate = this.updateQueue.shift();
198
- if (queuedUpdate) {
199
- queuedUpdate();
200
- }
201
- }
202
- }
203
-
204
- /**
205
- * Notify all subscribers
206
- */
207
- private notifySubscribers(prevState: T): void {
208
- for (const subscriber of this.subscribers.values()) {
209
- try {
210
- if (subscriber.selector) {
211
- const currentValue = subscriber.selector(this.state);
212
- const prevValue = subscriber.selector(prevState);
213
-
214
- if (currentValue !== prevValue) {
215
- subscriber.callback(this.state, prevState);
216
- }
217
- } else {
218
- subscriber.callback(this.state, prevState);
219
- }
220
- } catch (error) {
221
- console.error(`Error in store subscriber ${subscriber.id}:`, error);
222
- }
223
- }
224
- }
225
-
226
- /**
227
- * Get debug information
228
- */
229
- getDebugInfo() {
230
- return {
231
- state: this.getState(),
232
- actions: Object.keys(this.actions),
233
- subscribers: this.subscribers.size,
234
- middlewares: this.middlewares.map(m => m.name)
235
- };
236
- }
237
- }