@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.
@@ -0,0 +1,640 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components';
2
+ import {
3
+ Store,
4
+ globalState,
5
+ createLoggerMiddleware,
6
+ createPersistenceMiddleware,
7
+ createCacheMiddleware,
8
+ createDevToolsMiddleware,
9
+ StoreState
10
+ } from '@miura/miura-data-flow';
11
+ import { html } from '@miura/miura-element';
12
+ import { MiuraElement } from '@miura/miura-element';
13
+
14
+ // Define the store state interface
15
+ interface DemoStoreState extends StoreState {
16
+ count: number;
17
+ user: any;
18
+ todos: Array<{ id: number; text: string; completed: boolean }>;
19
+ loading: boolean;
20
+ error: any;
21
+ }
22
+
23
+ const meta: Meta = {
24
+ title: 'miura-data-flow/Data Flow Demo',
25
+ parameters: {
26
+ docs: {
27
+ description: {
28
+ component: `
29
+ # miura Data Flow Demo
30
+
31
+ This story demonstrates the comprehensive state management features of miura Data Flow:
32
+
33
+ ## 🏪 Store Management
34
+ - Reactive state management with subscriptions
35
+ - Action-based state updates
36
+ - Immutable state updates
37
+ - Type-safe state access
38
+
39
+ ## 🌍 Global State
40
+ - Application-wide state management
41
+ - Component-specific subscriptions
42
+ - Cross-component communication
43
+
44
+ ## 🔌 Middleware System
45
+ - Logger middleware for debugging
46
+ - Persistence middleware for localStorage
47
+ - Cache middleware for API responses
48
+ - DevTools middleware for Redux DevTools
49
+
50
+ ## 📡 Reactive Updates
51
+ - Automatic UI updates on state changes
52
+ - Selective subscriptions to specific properties
53
+ - Performance-optimized change detection
54
+
55
+ ## Usage Examples
56
+
57
+ \`\`\`typescript
58
+ import { Store, globalState, createLoggerMiddleware } from '@miura/miura-data-flow';
59
+
60
+ // Create a store
61
+ const store = new Store({ count: 0, user: null });
62
+
63
+ // Define actions
64
+ store.defineActions({
65
+ increment: (state) => ({ count: state.count + 1 }),
66
+ setUser: (state, user) => ({ user })
67
+ });
68
+
69
+ // Add middleware
70
+ store.use(createLoggerMiddleware());
71
+
72
+ // Subscribe to changes
73
+ const unsubscribe = store.subscribe((state, prevState) => {
74
+ console.log('State changed:', state);
75
+ });
76
+
77
+ // Global state
78
+ globalState.set('theme', 'dark');
79
+ globalState.subscribeTo('my-component', 'theme', (theme) => {
80
+ console.log('Theme changed:', theme);
81
+ });
82
+ \`\`\`
83
+ `
84
+ }
85
+ }
86
+ }
87
+ };
88
+
89
+ export default meta;
90
+ type Story = StoryObj;
91
+
92
+ // Data Flow demo component
93
+ class DataFlowDemo extends MiuraElement {
94
+ private store: Store<DemoStoreState>;
95
+ private globalState = globalState;
96
+ private storeEvents: any[] = [];
97
+ private globalEvents: any[] = [];
98
+ private unsubscribeStore: (() => void) | null = null;
99
+ private unsubscribeGlobal: (() => void) | null = null;
100
+
101
+ constructor() {
102
+ super();
103
+ this.initializeStore();
104
+ this.setupGlobalState();
105
+ }
106
+
107
+ connectedCallback() {
108
+ super.connectedCallback();
109
+ this.setupEventListeners();
110
+ }
111
+
112
+ disconnectedCallback() {
113
+ if (this.unsubscribeStore) {
114
+ this.unsubscribeStore();
115
+ }
116
+ if (this.unsubscribeGlobal) {
117
+ this.unsubscribeGlobal();
118
+ }
119
+ }
120
+
121
+ private initializeStore() {
122
+ // Create store with initial state
123
+ this.store = new Store<DemoStoreState>({
124
+ count: 0,
125
+ user: null,
126
+ todos: [],
127
+ loading: false,
128
+ error: null
129
+ });
130
+
131
+ // Define actions — each receives (state, ...args) and returns Partial<T>
132
+ this.store.defineActions({
133
+ increment: (state) => ({ count: state.count + 1 }),
134
+ decrement: (state) => ({ count: state.count - 1 }),
135
+ setUser: (_state, ...args: unknown[]) => ({ user: args[0] }),
136
+ addTodo: (state, ...args: unknown[]) => ({
137
+ todos: [...state.todos, { id: Date.now(), text: args[0] as string, completed: false }]
138
+ }),
139
+ toggleTodo: (state, ...args: unknown[]) => ({
140
+ todos: state.todos.map(todo =>
141
+ todo.id === (args[0] as number) ? { ...todo, completed: !todo.completed } : todo
142
+ )
143
+ }),
144
+ removeTodo: (state, ...args: unknown[]) => ({
145
+ todos: state.todos.filter(todo => todo.id !== (args[0] as number))
146
+ }),
147
+ setLoading: (_state, ...args: unknown[]) => ({ loading: args[0] as boolean }),
148
+ setError: (_state, ...args: unknown[]) => ({ error: args[0] })
149
+ });
150
+
151
+ // Add middleware
152
+ this.store.use(createLoggerMiddleware());
153
+ this.store.use(createPersistenceMiddleware(['user', 'todos']));
154
+ this.store.use(createCacheMiddleware(30000)); // 30 seconds
155
+ this.store.use(createDevToolsMiddleware('DataFlowDemo'));
156
+
157
+ // Subscribe to store changes
158
+ this.unsubscribeStore = this.store.subscribe((state, prevState) => {
159
+ this.addStoreEvent('State Updated', { state, prevState });
160
+ this.requestUpdate();
161
+ });
162
+ }
163
+
164
+ private setupGlobalState() {
165
+ // Subscribe to global state changes
166
+ this.unsubscribeGlobal = this.globalState.subscribe(
167
+ 'data-flow-demo',
168
+ ['theme', 'language', 'notifications'],
169
+ (state, prevState) => {
170
+ this.addGlobalEvent('Global State Updated', { state, prevState });
171
+ this.requestUpdate();
172
+ }
173
+ );
174
+ }
175
+
176
+ private setupEventListeners() {
177
+ this.shadowRoot!.addEventListener('click', (e) => {
178
+ const target = (e.composedPath()[0] || e.target) as HTMLElement;
179
+
180
+ if (target.matches('[data-action="increment"]')) {
181
+ this.store.dispatch('increment');
182
+ } else if (target.matches('[data-action="decrement"]')) {
183
+ this.store.dispatch('decrement');
184
+ } else if (target.matches('[data-action="set-user"]')) {
185
+ this.store.dispatch('setUser', { id: '1', name: 'John Doe', email: 'john@example.com' });
186
+ } else if (target.matches('[data-action="clear-user"]')) {
187
+ this.store.dispatch('setUser', null);
188
+ } else if (target.matches('[data-action="add-todo"]')) {
189
+ const input = this.shadowRoot!.querySelector('[name="todo-input"]') as HTMLInputElement;
190
+ if (input.value.trim()) {
191
+ this.store.dispatch('addTodo', input.value.trim());
192
+ input.value = '';
193
+ }
194
+ } else if (target.matches('[data-action="toggle-theme"]')) {
195
+ const currentTheme = this.globalState.get('theme');
196
+ this.globalState.set('theme', currentTheme === 'light' ? 'dark' : 'light');
197
+ } else if (target.matches('[data-action="set-language"]')) {
198
+ const select = this.shadowRoot!.querySelector('[name="language"]') as HTMLSelectElement;
199
+ this.globalState.set('language', select.value);
200
+ } else if (target.matches('[data-action="add-notification"]')) {
201
+ const notifications = this.globalState.get('notifications') || [];
202
+ const newNotification = {
203
+ id: Date.now().toString(),
204
+ message: `Notification ${notifications.length + 1}`,
205
+ type: ['info', 'success', 'warning', 'error'][Math.floor(Math.random() * 4)] as any
206
+ };
207
+ this.globalState.set('notifications', [...notifications, newNotification]);
208
+ } else if (target.matches('[data-action="clear-notifications"]')) {
209
+ this.globalState.set('notifications', []);
210
+ } else if (target.matches('[data-action="clear-store-events"]')) {
211
+ this.storeEvents = [];
212
+ this.requestUpdate();
213
+ } else if (target.matches('[data-action="clear-global-events"]')) {
214
+ this.globalEvents = [];
215
+ this.requestUpdate();
216
+ } else if (target.matches('[data-action="toggle-todo"]')) {
217
+ const id = parseInt(target.getAttribute('data-id') || '0');
218
+ this.store.dispatch('toggleTodo', id);
219
+ } else if (target.matches('[data-action="remove-todo"]')) {
220
+ const id = parseInt(target.getAttribute('data-id') || '0');
221
+ this.store.dispatch('removeTodo', id);
222
+ }
223
+ });
224
+ }
225
+
226
+ private addStoreEvent(type: string, details: any) {
227
+ this.storeEvents.unshift({
228
+ type,
229
+ details,
230
+ timestamp: new Date().toLocaleTimeString()
231
+ });
232
+
233
+ // Keep only last 10 events
234
+ if (this.storeEvents.length > 10) {
235
+ this.storeEvents = this.storeEvents.slice(0, 10);
236
+ }
237
+ }
238
+
239
+ private addGlobalEvent(type: string, details: any) {
240
+ this.globalEvents.unshift({
241
+ type,
242
+ details,
243
+ timestamp: new Date().toLocaleTimeString()
244
+ });
245
+
246
+ // Keep only last 10 events
247
+ if (this.globalEvents.length > 10) {
248
+ this.globalEvents = this.globalEvents.slice(0, 10);
249
+ }
250
+ }
251
+
252
+ template() {
253
+ const storeState = this.store.getState();
254
+ const globalStateData = {
255
+ theme: this.globalState.get('theme'),
256
+ language: this.globalState.get('language'),
257
+ notifications: this.globalState.get('notifications') || []
258
+ };
259
+
260
+ return html`
261
+ <style>
262
+ :host {
263
+ display: block;
264
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
265
+ max-width: 1400px;
266
+ margin: 0 auto;
267
+ padding: 20px;
268
+ }
269
+
270
+ .container {
271
+ display: grid;
272
+ grid-template-columns: 1fr 1fr;
273
+ gap: 20px;
274
+ }
275
+
276
+ .section {
277
+ background: #f8f9fa;
278
+ border-radius: 8px;
279
+ padding: 20px;
280
+ border: 1px solid #e9ecef;
281
+ }
282
+
283
+ .section h2 {
284
+ margin-top: 0;
285
+ color: #495057;
286
+ border-bottom: 2px solid #007bff;
287
+ padding-bottom: 10px;
288
+ }
289
+
290
+ .form-group {
291
+ margin-bottom: 15px;
292
+ }
293
+
294
+ label {
295
+ display: block;
296
+ margin-bottom: 5px;
297
+ font-weight: 500;
298
+ color: #495057;
299
+ }
300
+
301
+ input, select, textarea {
302
+ width: 100%;
303
+ padding: 8px 12px;
304
+ border: 1px solid #ced4da;
305
+ border-radius: 4px;
306
+ font-size: 14px;
307
+ box-sizing: border-box;
308
+ }
309
+
310
+ button {
311
+ background: #007bff;
312
+ color: white;
313
+ border: none;
314
+ padding: 8px 16px;
315
+ border-radius: 4px;
316
+ cursor: pointer;
317
+ font-size: 14px;
318
+ margin-right: 8px;
319
+ margin-bottom: 8px;
320
+ }
321
+
322
+ button:hover {
323
+ background: #0056b3;
324
+ }
325
+
326
+ button.secondary {
327
+ background: #6c757d;
328
+ }
329
+
330
+ button.secondary:hover {
331
+ background: #545b62;
332
+ }
333
+
334
+ button.success {
335
+ background: #28a745;
336
+ }
337
+
338
+ button.success:hover {
339
+ background: #1e7e34;
340
+ }
341
+
342
+ button.danger {
343
+ background: #dc3545;
344
+ }
345
+
346
+ button.danger:hover {
347
+ background: #c82333;
348
+ }
349
+
350
+ .state-display {
351
+ background: #fff;
352
+ border: 1px solid #dee2e6;
353
+ border-radius: 4px;
354
+ padding: 15px;
355
+ margin-bottom: 15px;
356
+ font-family: 'Courier New', monospace;
357
+ font-size: 12px;
358
+ max-height: 200px;
359
+ overflow-y: auto;
360
+ }
361
+
362
+ .events {
363
+ max-height: 300px;
364
+ overflow-y: auto;
365
+ background: #fff;
366
+ border: 1px solid #dee2e6;
367
+ border-radius: 4px;
368
+ padding: 10px;
369
+ }
370
+
371
+ .event {
372
+ background: #f8f9fa;
373
+ border-left: 4px solid #007bff;
374
+ padding: 8px;
375
+ margin-bottom: 8px;
376
+ font-size: 12px;
377
+ }
378
+
379
+ .event.store {
380
+ border-left-color: #28a745;
381
+ }
382
+
383
+ .event.global {
384
+ border-left-color: #ffc107;
385
+ }
386
+
387
+ .event-time {
388
+ color: #6c757d;
389
+ font-size: 11px;
390
+ }
391
+
392
+ .todo-item {
393
+ display: flex;
394
+ align-items: center;
395
+ padding: 8px;
396
+ border: 1px solid #dee2e6;
397
+ border-radius: 4px;
398
+ margin-bottom: 8px;
399
+ background: #fff;
400
+ }
401
+
402
+ .todo-item.completed {
403
+ opacity: 0.6;
404
+ text-decoration: line-through;
405
+ }
406
+
407
+ .todo-text {
408
+ flex: 1;
409
+ margin: 0 10px;
410
+ }
411
+
412
+ .notification {
413
+ padding: 8px 12px;
414
+ border-radius: 4px;
415
+ margin-bottom: 8px;
416
+ font-size: 12px;
417
+ }
418
+
419
+ .notification.info {
420
+ background: #d1ecf1;
421
+ color: #0c5460;
422
+ border: 1px solid #bee5eb;
423
+ }
424
+
425
+ .notification.success {
426
+ background: #d4edda;
427
+ color: #155724;
428
+ border: 1px solid #c3e6cb;
429
+ }
430
+
431
+ .notification.warning {
432
+ background: #fff3cd;
433
+ color: #856404;
434
+ border: 1px solid #ffeaa7;
435
+ }
436
+
437
+ .notification.error {
438
+ background: #f8d7da;
439
+ color: #721c24;
440
+ border: 1px solid #f5c6cb;
441
+ }
442
+
443
+ .stats {
444
+ display: grid;
445
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
446
+ gap: 10px;
447
+ margin-bottom: 15px;
448
+ }
449
+
450
+ .stat {
451
+ background: #fff;
452
+ padding: 10px;
453
+ border-radius: 4px;
454
+ text-align: center;
455
+ border: 1px solid #dee2e6;
456
+ }
457
+
458
+ .stat-value {
459
+ font-size: 24px;
460
+ font-weight: bold;
461
+ color: #007bff;
462
+ }
463
+
464
+ .stat-label {
465
+ font-size: 12px;
466
+ color: #6c757d;
467
+ margin-top: 5px;
468
+ }
469
+ </style>
470
+
471
+ <div class="container">
472
+ <div class="section">
473
+ <h2>🏪 Store Management</h2>
474
+
475
+ <div class="stats">
476
+ <div class="stat">
477
+ <div class="stat-value">${storeState.count}</div>
478
+ <div class="stat-label">Count</div>
479
+ </div>
480
+ <div class="stat">
481
+ <div class="stat-value">${storeState.todos.length}</div>
482
+ <div class="stat-label">Todos</div>
483
+ </div>
484
+ <div class="stat">
485
+ <div class="stat-value">${storeState.todos.filter(t => t.completed).length}</div>
486
+ <div class="stat-label">Completed</div>
487
+ </div>
488
+ <div class="stat">
489
+ <div class="stat-value">${storeState.user ? 'Yes' : 'No'}</div>
490
+ <div class="stat-label">User</div>
491
+ </div>
492
+ </div>
493
+
494
+ <div class="form-group">
495
+ <button data-action="increment">Increment</button>
496
+ <button data-action="decrement">Decrement</button>
497
+ </div>
498
+
499
+ <div class="form-group">
500
+ <button data-action="set-user" class="success">Set User</button>
501
+ <button data-action="clear-user" class="danger">Clear User</button>
502
+ </div>
503
+
504
+ <div class="form-group">
505
+ <label>Add Todo:</label>
506
+ <div style="display: flex; gap: 8px;">
507
+ <input type="text" name="todo-input" placeholder="Enter todo...">
508
+ <button data-action="add-todo">Add</button>
509
+ </div>
510
+ </div>
511
+
512
+ <div class="state-display">
513
+ <strong>Store State:</strong><br>
514
+ <pre>${JSON.stringify(storeState, null, 2)}</pre>
515
+ </div>
516
+ </div>
517
+
518
+ <div class="section">
519
+ <h2>🌍 Global State</h2>
520
+
521
+ <div class="form-group">
522
+ <label>Theme:</label>
523
+ <button data-action="toggle-theme">
524
+ Toggle Theme (${globalStateData.theme})
525
+ </button>
526
+ </div>
527
+
528
+ <div class="form-group">
529
+ <label>Language:</label>
530
+ <select name="language">
531
+ <option value="en" ${globalStateData.language === 'en' ? 'selected' : ''}>English</option>
532
+ <option value="es" ${globalStateData.language === 'es' ? 'selected' : ''}>Spanish</option>
533
+ <option value="fr" ${globalStateData.language === 'fr' ? 'selected' : ''}>French</option>
534
+ <option value="de" ${globalStateData.language === 'de' ? 'selected' : ''}>German</option>
535
+ </select>
536
+ <button data-action="set-language">Set Language</button>
537
+ </div>
538
+
539
+ <div class="form-group">
540
+ <button data-action="add-notification" class="success">Add Notification</button>
541
+ <button data-action="clear-notifications" class="secondary">Clear All</button>
542
+ </div>
543
+
544
+ <div class="state-display">
545
+ <strong>Global State:</strong><br>
546
+ <pre>${JSON.stringify(globalStateData, null, 2)}</pre>
547
+ </div>
548
+ </div>
549
+
550
+ <div class="section">
551
+ <h2>📝 Todo List</h2>
552
+ ${storeState.todos.length === 0 ?
553
+ html`<p style="color: #6c757d; text-align: center;">No todos yet. Add one above!</p>` :
554
+ storeState.todos.map(todo => html`
555
+ <div class="todo-item ${todo.completed ? 'completed' : ''}">
556
+ <button data-action="toggle-todo" data-id="${todo.id}" class="secondary">
557
+ ${todo.completed ? '✓' : '○'}
558
+ </button>
559
+ <span class="todo-text">${todo.text}</span>
560
+ <button data-action="remove-todo" data-id="${todo.id}" class="danger">×</button>
561
+ </div>
562
+ `)
563
+ }
564
+ </div>
565
+
566
+ <div class="section">
567
+ <h2>🔔 Notifications</h2>
568
+ ${globalStateData.notifications.length === 0 ?
569
+ html`<p style="color: #6c757d; text-align: center;">No notifications</p>` :
570
+ globalStateData.notifications.map(notification => html`
571
+ <div class="notification ${notification.type}">
572
+ ${notification.message}
573
+ </div>
574
+ `)
575
+ }
576
+ </div>
577
+
578
+ <div class="section" style="grid-column: 1 / -1;">
579
+ <h2>📊 Store Events</h2>
580
+ <button data-action="clear-store-events" class="secondary">Clear Events</button>
581
+ <div class="events">
582
+ ${this.storeEvents.length === 0 ?
583
+ html`<div style="color: #6c757d; text-align: center; padding: 20px;">No store events yet</div>` :
584
+ this.storeEvents.map(event => html`
585
+ <div class="event store">
586
+ <div class="event-time">${event.timestamp}</div>
587
+ <strong>${event.type}</strong><br>
588
+ <pre style="font-size: 11px; margin: 5px 0 0 0; white-space: pre-wrap;">${JSON.stringify(event.details, null, 2)}</pre>
589
+ </div>
590
+ `)
591
+ }
592
+ </div>
593
+ </div>
594
+
595
+ <div class="section" style="grid-column: 1 / -1;">
596
+ <h2>🌍 Global Events</h2>
597
+ <button data-action="clear-global-events" class="secondary">Clear Events</button>
598
+ <div class="events">
599
+ ${this.globalEvents.length === 0 ?
600
+ html`<div style="color: #6c757d; text-align: center; padding: 20px;">No global events yet</div>` :
601
+ this.globalEvents.map(event => html`
602
+ <div class="event global">
603
+ <div class="event-time">${event.timestamp}</div>
604
+ <strong>${event.type}</strong><br>
605
+ <pre style="font-size: 11px; margin: 5px 0 0 0; white-space: pre-wrap;">${JSON.stringify(event.details, null, 2)}</pre>
606
+ </div>
607
+ `)
608
+ }
609
+ </div>
610
+ </div>
611
+ </div>
612
+ `;
613
+ }
614
+ }
615
+
616
+ customElements.define('data-flow-demo', DataFlowDemo);
617
+
618
+ export const Default: Story = {
619
+ render: () => `
620
+ <data-flow-demo></data-flow-demo>
621
+ `
622
+ };
623
+
624
+ export const WithInitialData: Story = {
625
+ render: () => `
626
+ <data-flow-demo></data-flow-demo>
627
+ `,
628
+ play: async () => {
629
+ // Initialize with some data
630
+ const { globalState } = await import('@miura/miura-data-flow');
631
+
632
+ // Set initial global state
633
+ globalState.set('theme', 'dark');
634
+ globalState.set('language', 'es');
635
+ globalState.set('notifications', [
636
+ { id: '1', message: 'Welcome to miura Data Flow!', type: 'success' },
637
+ { id: '2', message: 'This is a demo notification', type: 'info' }
638
+ ]);
639
+ }
640
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": ".",
6
+ "composite": true,
7
+ "baseUrl": ".",
8
+ "paths": {
9
+ "@miura/miura-debugger": ["../miura-debugger/index.ts"]
10
+ }
11
+ },
12
+ "include": ["index.ts", "src/**/*"],
13
+ "exclude": ["node_modules"],
14
+ "references": [
15
+ { "path": "../miura-debugger" }
16
+ ]
17
+ }