snice 3.5.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/bin/snice.js +2 -1
  2. package/bin/templates/CLAUDE.md +24 -2
  3. package/bin/templates/pwa/README.md +173 -0
  4. package/bin/templates/pwa/global.d.ts +10 -0
  5. package/bin/templates/pwa/index.html +17 -0
  6. package/bin/templates/pwa/package.json +25 -0
  7. package/bin/templates/pwa/public/icons/.gitkeep +6 -0
  8. package/bin/templates/pwa/public/manifest.json +24 -0
  9. package/bin/templates/pwa/public/vite.svg +1 -0
  10. package/bin/templates/pwa/src/daemons/notifications.ts +148 -0
  11. package/bin/templates/pwa/src/guards/auth.ts +10 -0
  12. package/bin/templates/pwa/src/main.ts +42 -0
  13. package/bin/templates/pwa/src/middleware/auth.ts +15 -0
  14. package/bin/templates/pwa/src/middleware/error.ts +25 -0
  15. package/bin/templates/pwa/src/middleware/retry.ts +27 -0
  16. package/bin/templates/pwa/src/pages/dashboard.ts +142 -0
  17. package/bin/templates/pwa/src/pages/login.ts +161 -0
  18. package/bin/templates/pwa/src/pages/notifications.ts +156 -0
  19. package/bin/templates/pwa/src/pages/profile.ts +163 -0
  20. package/bin/templates/pwa/src/router.ts +16 -0
  21. package/bin/templates/pwa/src/services/auth.ts +48 -0
  22. package/bin/templates/pwa/src/services/jwt.ts +35 -0
  23. package/bin/templates/pwa/src/services/storage.ts +24 -0
  24. package/bin/templates/pwa/src/styles/global.css +55 -0
  25. package/bin/templates/pwa/src/types/auth.ts +21 -0
  26. package/bin/templates/pwa/src/types/notifications.ts +9 -0
  27. package/bin/templates/pwa/src/utils/fetch.ts +39 -0
  28. package/bin/templates/pwa/tsconfig.json +23 -0
  29. package/bin/templates/pwa/vite.config.ts +94 -0
  30. package/dist/components/audio-recorder/snice-audio-recorder.d.ts +14 -4
  31. package/dist/components/audio-recorder/snice-audio-recorder.js +248 -71
  32. package/dist/components/audio-recorder/snice-audio-recorder.js.map +1 -1
  33. package/dist/components/audio-recorder/snice-audio-recorder.types.d.ts +2 -0
  34. package/dist/components/music-player/snice-music-player.d.ts +72 -0
  35. package/dist/components/music-player/snice-music-player.js +730 -0
  36. package/dist/components/music-player/snice-music-player.js.map +1 -0
  37. package/dist/components/music-player/snice-music-player.types.d.ts +43 -0
  38. package/dist/components/timer/snice-timer.d.ts +27 -0
  39. package/dist/components/timer/snice-timer.js +197 -0
  40. package/dist/components/timer/snice-timer.js.map +1 -0
  41. package/dist/components/timer/snice-timer.types.d.ts +10 -0
  42. package/dist/fetcher.d.ts +65 -0
  43. package/dist/index.cjs +92 -3
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.esm.js +92 -4
  47. package/dist/index.esm.js.map +1 -1
  48. package/dist/index.iife.js +92 -3
  49. package/dist/index.iife.js.map +1 -1
  50. package/dist/symbols.cjs +1 -1
  51. package/dist/symbols.esm.js +1 -1
  52. package/dist/transitions.cjs +1 -1
  53. package/dist/transitions.esm.js +1 -1
  54. package/dist/types/context.d.ts +7 -1
  55. package/dist/types/router-options.d.ts +6 -0
  56. package/docs/ai/api.md +33 -1
  57. package/docs/ai/components/music-player.md +134 -0
  58. package/docs/ai/components/terminal.md +147 -0
  59. package/docs/ai/components/timer.md +43 -0
  60. package/docs/ai/patterns.md +47 -0
  61. package/docs/components/music-player.md +314 -0
  62. package/docs/components/terminal.md +451 -0
  63. package/docs/components/timer.md +143 -0
  64. package/docs/fetcher.md +447 -0
  65. package/docs/routing.md +2 -0
  66. package/package.json +2 -1
package/bin/snice.js CHANGED
@@ -47,6 +47,7 @@ Options:
47
47
  Templates:
48
48
  base - Minimal starter with counter example (default)
49
49
  social - Social media sample app showcasing components
50
+ pwa - Progressive Web App with auth, middleware, and live notifications
50
51
 
51
52
  Examples:
52
53
  snice create-app my-app
@@ -90,7 +91,7 @@ function createApp(projectPath, template = 'base') {
90
91
  // Check if template exists
91
92
  if (!existsSync(templateDir)) {
92
93
  console.error(`❌ Template "${template}" not found!`);
93
- console.error(`Available templates: base, social`);
94
+ console.error(`Available templates: base, social, pwa`);
94
95
  process.exit(1);
95
96
  }
96
97
 
@@ -17,13 +17,23 @@ src/
17
17
  pages/ # @page decorated route components
18
18
  components/ # @element decorated UI components
19
19
  controllers/ # @controller decorated behavior modules
20
+ utils/ # Pure functions, helpers (formatDate, debounce)
21
+ services/ # Stateless static functions for business logic/external APIs
22
+ daemons/ # Stateful classes with lifecycle (start/stop/dispose)
23
+ middleware/ # Composable middleware (fetch, logging, etc)
24
+ guards/ # Route guards
25
+ types/ # TypeScript types
20
26
  styles/ # Global CSS
21
27
  ```
22
28
 
23
29
  **Separation of concerns:**
24
30
  - **Pages** - Orchestrate elements, handle URLs
25
- - **Elements** - Pure presentation, no business logic
26
- - **Controllers** - Behavior, data fetching, swappable
31
+ - **Components** - Pure presentation, no business logic
32
+ - **Controllers** - Attach to elements, add behavior
33
+ - **Utils** - Pure helper functions
34
+ - **Services** - Stateless business logic, API calls
35
+ - **Daemons** - Lifecycle-managed (WebSocket, P2P, intervals)
36
+ - **Middleware** - Composable functions (auth, logging, retry)
27
37
 
28
38
  ## Decorators
29
39
 
@@ -129,6 +139,18 @@ html`
129
139
  - **Element ↔ Controller:** Request/Response (`@request`, `@respond`)
130
140
  - **Global State:** Context (`@context()`)
131
141
 
142
+ ## Common Mistakes
143
+
144
+ **Controllers are NOT global services.** They attach to elements via `controller="name"`. Use `utils/` for shared logic like API calls, auth, toasts.
145
+
146
+ **@request/@respond is NOT a service bus.** It's for element-to-element or element-to-controller communication only. Use utility functions for app-wide features.
147
+
148
+ **Guards receive Context<T>, not T.** Check `ctx.application.property`, not `ctx.property`. Guards: `(ctx: Context<AppContext>) => boolean`
149
+
150
+ **Context must be mutated then updated.** After changing `ctx.application`, call `ctx.update()` to notify subscribers. Pages need `@context()` to get context reference.
151
+
152
+ **@property is for parent-provided attributes.** Internal component state should be regular properties, not decorated. Only use `@property()` when the value comes from a parent element via attributes.
153
+
132
154
  ## Build Commands
133
155
 
134
156
  ```bash
@@ -0,0 +1,173 @@
1
+ # {{projectName}}
2
+
3
+ A Progressive Web App (PWA) built with [Snice](https://github.com/sniceio/snice).
4
+
5
+ ## Features
6
+
7
+ - ⚡ **JWT Authentication** - Token-based auth with automatic refresh
8
+ - 🔒 **Protected Routes** - Route guards for authenticated pages
9
+ - 🎯 **Middleware Pattern** - Composable fetch middleware (auth, error, retry)
10
+ - 📱 **PWA Ready** - Service worker, offline support, installable
11
+ - 🔔 **Live Notifications** - WebSocket daemon for real-time updates
12
+ - 🎨 **Snice Components** - Pre-built UI components
13
+ - 📦 **Type-Safe** - Full TypeScript support
14
+ - 🚀 **Fast Build** - Vite + SWC for blazing fast dev and builds
15
+
16
+ ## Getting Started
17
+
18
+ ```bash
19
+ # Install dependencies
20
+ npm install
21
+
22
+ # Start dev server
23
+ npm run dev
24
+
25
+ # Build for production
26
+ npm run build
27
+
28
+ # Preview production build
29
+ npm run preview
30
+
31
+ # Type check
32
+ npm run type-check
33
+ ```
34
+
35
+ ## Demo Credentials
36
+
37
+ - **Email:** demo@example.com
38
+ - **Password:** demo
39
+
40
+ ## Project Structure
41
+
42
+ ```
43
+ src/
44
+ utils/ # Pure helper functions
45
+ services/ # Business logic (auth, storage, jwt)
46
+ middleware/ # Fetch middleware (auth, error, retry)
47
+ daemons/ # Lifecycle-managed classes (notifications WebSocket)
48
+ guards/ # Route guards (auth)
49
+ types/ # TypeScript types
50
+ pages/ # Routable pages (@page decorator)
51
+ styles/ # Global styles
52
+ ```
53
+
54
+ ## Architecture Patterns
55
+
56
+ ### Middleware-Based Fetch
57
+
58
+ Composable middleware for all API calls:
59
+
60
+ ```typescript
61
+ import { createFetch } from './utils/fetch';
62
+ import { authMiddleware } from './middleware/auth';
63
+ import { errorMiddleware } from './middleware/error';
64
+ import { retryMiddleware } from './middleware/retry';
65
+
66
+ export const api = createFetch({
67
+ baseURL: import.meta.env.VITE_API_URL,
68
+ middleware: [authMiddleware, errorMiddleware, retryMiddleware()],
69
+ headers: { 'Content-Type': 'application/json' }
70
+ });
71
+
72
+ // Usage
73
+ const data = await api('/users').then(r => r.json());
74
+ ```
75
+
76
+ ### Daemons for Lifecycle Management
77
+
78
+ Use daemons for resources that need start/stop/dispose:
79
+
80
+ ```typescript
81
+ const daemon = getNotificationsDaemon();
82
+ daemon.start(); // In main.ts
83
+
84
+ // In component
85
+ const unsubscribe = daemon.subscribe((notification) => {
86
+ console.log(notification);
87
+ });
88
+
89
+ // Cleanup
90
+ unsubscribe();
91
+ ```
92
+
93
+ ### Route Guards
94
+
95
+ Protect routes with guards:
96
+
97
+ ```typescript
98
+ import { authGuard } from './guards/auth';
99
+
100
+ @page({
101
+ tag: 'dashboard-page',
102
+ routes: ['/dashboard'],
103
+ guards: [authGuard]
104
+ })
105
+ export class DashboardPage extends HTMLElement {
106
+ // ...
107
+ }
108
+ ```
109
+
110
+ ### Context for Global State
111
+
112
+ Access shared state via context:
113
+
114
+ ```typescript
115
+ @context()
116
+ handleContext(ctx: Context<AppContext>) {
117
+ this.user = ctx.application.user;
118
+ }
119
+ ```
120
+
121
+ ## Customization
122
+
123
+ ### Replace Mock API
124
+
125
+ Update `src/services/auth.ts` to call your real API:
126
+
127
+ ```typescript
128
+ export async function login(credentials: LoginCredentials) {
129
+ const response = await fetch('https://your-api.com/auth/login', {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/json' },
132
+ body: JSON.stringify(credentials)
133
+ });
134
+
135
+ const data = await response.json();
136
+ setToken(data.token);
137
+ setUser(data.user);
138
+ return data;
139
+ }
140
+ ```
141
+
142
+ ### Enable Real WebSocket
143
+
144
+ Update `src/daemons/notifications.ts`:
145
+
146
+ ```typescript
147
+ // Replace startMockNotifications() with:
148
+ this.connect();
149
+ ```
150
+
151
+ ### Configure Environment
152
+
153
+ Create `.env` file:
154
+
155
+ ```
156
+ VITE_API_URL=https://your-api.com
157
+ VITE_WS_URL=wss://your-ws.com
158
+ ```
159
+
160
+ ### Update Icons
161
+
162
+ Replace placeholder icons in `public/icons/`:
163
+ - `icon-192.png` (192x192)
164
+ - `icon-512.png` (512x512)
165
+
166
+ ## Learn More
167
+
168
+ - [Snice Documentation](https://sniceio.github.io/snice)
169
+ - [Vite PWA Plugin](https://vite-pwa-org.netlify.app/)
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,10 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_API_URL?: string;
5
+ readonly VITE_WS_URL?: string;
6
+ }
7
+
8
+ interface ImportMeta {
9
+ readonly env: ImportMetaEnv;
10
+ }
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="{{projectName}} - PWA built with Snice" />
7
+ <meta name="theme-color" content="#6366f1" />
8
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
9
+ <link rel="manifest" href="/manifest.json" />
10
+ <link rel="apple-touch-icon" href="/icons/icon-192.png" />
11
+ <title>{{projectName}}</title>
12
+ </head>
13
+ <body>
14
+ <div id="app"></div>
15
+ <script type="module" src="/src/main.ts"></script>
16
+ </body>
17
+ </html>
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "type-check": "tsc --noEmit",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "snice": "^3.0.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20.0.0",
17
+ "@vite-pwa/assets-generator": "^0.2.4",
18
+ "terser": "^5.24.0",
19
+ "typescript": "^5.3.3",
20
+ "unplugin-swc": "^1.5.7",
21
+ "vite": "^5.0.10",
22
+ "vite-plugin-pwa": "^0.17.4",
23
+ "workbox-window": "^7.0.0"
24
+ }
25
+ }
@@ -0,0 +1,6 @@
1
+ # Icons directory
2
+ # Replace these with your actual app icons:
3
+ # - icon-192.png (192x192)
4
+ # - icon-512.png (512x512)
5
+
6
+ # For now, vite-plugin-pwa will generate placeholder icons during build
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "short_name": "{{projectName}}",
4
+ "description": "PWA built with Snice",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#ffffff",
8
+ "theme_color": "#6366f1",
9
+ "orientation": "portrait-primary",
10
+ "icons": [
11
+ {
12
+ "src": "/icons/icon-192.png",
13
+ "sizes": "192x192",
14
+ "type": "image/png",
15
+ "purpose": "any maskable"
16
+ },
17
+ {
18
+ "src": "/icons/icon-512.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png",
21
+ "purpose": "any maskable"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -0,0 +1,148 @@
1
+ import type { Notification } from '../types/notifications';
2
+
3
+ type NotificationHandler = (notification: Notification) => void;
4
+
5
+ export class NotificationsDaemon {
6
+ private ws: WebSocket | null = null;
7
+ private handlers: Set<NotificationHandler> = new Set();
8
+ private reconnectInterval: number = 5000;
9
+ private reconnectTimer: number | null = null;
10
+ private mockInterval: number | null = null;
11
+ private url: string;
12
+ private isStarted: boolean = false;
13
+
14
+ constructor(url: string) {
15
+ this.url = url;
16
+ }
17
+
18
+ start(): void {
19
+ if (this.isStarted) return;
20
+ this.isStarted = true;
21
+
22
+ // For demo purposes, use mock notifications instead of real WebSocket
23
+ // In production, uncomment the connect() line and remove startMockNotifications()
24
+ // this.connect();
25
+ this.startMockNotifications();
26
+ }
27
+
28
+ stop(): void {
29
+ if (!this.isStarted) return;
30
+ this.isStarted = false;
31
+
32
+ this.stopMockNotifications();
33
+
34
+ if (this.reconnectTimer !== null) {
35
+ clearTimeout(this.reconnectTimer);
36
+ this.reconnectTimer = null;
37
+ }
38
+
39
+ if (this.ws) {
40
+ this.ws.close();
41
+ this.ws = null;
42
+ }
43
+ }
44
+
45
+ dispose(): void {
46
+ this.stop();
47
+ this.handlers.clear();
48
+ }
49
+
50
+ subscribe(handler: NotificationHandler): () => void {
51
+ this.handlers.add(handler);
52
+ return () => this.handlers.delete(handler);
53
+ }
54
+
55
+ private connect(): void {
56
+ try {
57
+ this.ws = new WebSocket(this.url);
58
+
59
+ this.ws.onopen = () => {
60
+ console.log('WebSocket connected');
61
+ };
62
+
63
+ this.ws.onmessage = (event) => {
64
+ try {
65
+ const notification: Notification = JSON.parse(event.data);
66
+ this.notify(notification);
67
+ } catch (err) {
68
+ console.error('Failed to parse notification:', err);
69
+ }
70
+ };
71
+
72
+ this.ws.onerror = (error) => {
73
+ console.error('WebSocket error:', error);
74
+ };
75
+
76
+ this.ws.onclose = () => {
77
+ console.log('WebSocket disconnected');
78
+ if (this.isStarted) {
79
+ this.scheduleReconnect();
80
+ }
81
+ };
82
+ } catch (err) {
83
+ console.error('Failed to create WebSocket:', err);
84
+ if (this.isStarted) {
85
+ this.scheduleReconnect();
86
+ }
87
+ }
88
+ }
89
+
90
+ private scheduleReconnect(): void {
91
+ if (this.reconnectTimer !== null) return;
92
+
93
+ this.reconnectTimer = window.setTimeout(() => {
94
+ this.reconnectTimer = null;
95
+ this.connect();
96
+ }, this.reconnectInterval);
97
+ }
98
+
99
+ private notify(notification: Notification): void {
100
+ this.handlers.forEach(handler => {
101
+ try {
102
+ handler(notification);
103
+ } catch (err) {
104
+ console.error('Notification handler error:', err);
105
+ }
106
+ });
107
+ }
108
+
109
+ // Mock notifications for demo
110
+ private startMockNotifications(): void {
111
+ const messages = [
112
+ { title: 'Welcome!', message: 'Thanks for checking out the PWA template', type: 'info' as const },
113
+ { title: 'New Feature', message: 'Check out the notifications page', type: 'success' as const },
114
+ { title: 'System Update', message: 'A new version is available', type: 'info' as const },
115
+ { title: 'Reminder', message: 'Don\'t forget to star the repo!', type: 'warning' as const }
116
+ ];
117
+
118
+ let index = 0;
119
+ this.mockInterval = window.setInterval(() => {
120
+ const notification: Notification = {
121
+ id: `mock-${Date.now()}`,
122
+ ...messages[index % messages.length],
123
+ timestamp: new Date().toISOString()
124
+ };
125
+ this.notify(notification);
126
+ index++;
127
+ }, 10000); // Send notification every 10 seconds
128
+ }
129
+
130
+ private stopMockNotifications(): void {
131
+ if (this.mockInterval !== null) {
132
+ clearInterval(this.mockInterval);
133
+ this.mockInterval = null;
134
+ }
135
+ }
136
+ }
137
+
138
+ // Singleton instance
139
+ let instance: NotificationsDaemon | null = null;
140
+
141
+ export function getNotificationsDaemon(): NotificationsDaemon {
142
+ if (!instance) {
143
+ // In production, use actual WebSocket URL from env
144
+ const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8080';
145
+ instance = new NotificationsDaemon(wsUrl);
146
+ }
147
+ return instance;
148
+ }
@@ -0,0 +1,10 @@
1
+ import type { Context } from 'snice';
2
+ import type { AppContext } from '../types/auth';
3
+
4
+ export function authGuard(ctx: Context<AppContext>): boolean {
5
+ if (!ctx.application.isAuthenticated) {
6
+ window.location.href = '#/login';
7
+ return false;
8
+ }
9
+ return true;
10
+ }
@@ -0,0 +1,42 @@
1
+ import { initialize } from './router';
2
+ import './styles/global.css';
3
+
4
+ // Import snice layout and components
5
+ import 'snice/components/layout/snice-layout';
6
+ import 'snice/components/button/snice-button';
7
+ import 'snice/components/card/snice-card';
8
+ import 'snice/components/input/snice-input';
9
+ import 'snice/components/alert/snice-alert';
10
+ import 'snice/components/avatar/snice-avatar';
11
+ import 'snice/components/empty-state/snice-empty-state';
12
+ import 'snice/components/spinner/snice-spinner';
13
+
14
+ // Import pages
15
+ import './pages/login';
16
+ import './pages/dashboard';
17
+ import './pages/profile';
18
+ import './pages/notifications';
19
+
20
+ // Import and start daemons
21
+ import { getNotificationsDaemon } from './daemons/notifications';
22
+
23
+ // Start notifications daemon
24
+ const notificationsDaemon = getNotificationsDaemon();
25
+ notificationsDaemon.start();
26
+
27
+ // Initialize router
28
+ initialize();
29
+
30
+ // Register service worker (PWA)
31
+ if ('serviceWorker' in navigator) {
32
+ window.addEventListener('load', () => {
33
+ navigator.serviceWorker.register('/sw.js').then(
34
+ (registration) => {
35
+ console.log('Service Worker registered:', registration);
36
+ },
37
+ (error) => {
38
+ console.error('Service Worker registration failed:', error);
39
+ }
40
+ );
41
+ });
42
+ }
@@ -0,0 +1,15 @@
1
+ import type { Middleware } from '../utils/fetch';
2
+ import { getToken } from '../services/storage';
3
+
4
+ export async function authMiddleware(url: string, options: RequestInit, next: () => Promise<Response>): Promise<Response> {
5
+ const token = getToken();
6
+
7
+ if (token) {
8
+ options.headers = {
9
+ ...options.headers,
10
+ 'Authorization': `Bearer ${token}`,
11
+ };
12
+ }
13
+
14
+ return next();
15
+ }
@@ -0,0 +1,25 @@
1
+ import type { Middleware } from '../utils/fetch';
2
+ import { clearToken } from '../services/storage';
3
+
4
+ export async function errorMiddleware(url: string, options: RequestInit, next: () => Promise<Response>): Promise<Response> {
5
+ const response = await next();
6
+
7
+ // Handle 401 unauthorized - token expired or invalid
8
+ if (response.status === 401) {
9
+ clearToken();
10
+ window.location.href = '#/login';
11
+ throw new Error('Unauthorized - redirecting to login');
12
+ }
13
+
14
+ // Handle other errors
15
+ if (!response.ok) {
16
+ const contentType = response.headers.get('content-type');
17
+ if (contentType && contentType.includes('application/json')) {
18
+ const error = await response.json();
19
+ throw new Error(error.message || `Request failed with status ${response.status}`);
20
+ }
21
+ throw new Error(`Request failed with status ${response.status}`);
22
+ }
23
+
24
+ return response;
25
+ }
@@ -0,0 +1,27 @@
1
+ import type { Middleware } from '../utils/fetch';
2
+
3
+ export function retryMiddleware(retries = 3, delay = 1000): Middleware {
4
+ return async (url, options, next) => {
5
+ let lastError: Error;
6
+
7
+ for (let i = 0; i < retries; i++) {
8
+ try {
9
+ return await next();
10
+ } catch (err) {
11
+ lastError = err as Error;
12
+
13
+ // Don't retry on certain errors
14
+ if (err instanceof Error && err.message.includes('Unauthorized')) {
15
+ throw err;
16
+ }
17
+
18
+ // Wait before retrying (exponential backoff)
19
+ if (i < retries - 1) {
20
+ await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
21
+ }
22
+ }
23
+ }
24
+
25
+ throw lastError!;
26
+ };
27
+ }