snice 3.6.0 → 3.8.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 (80) hide show
  1. package/README.md +2 -2
  2. package/bin/snice.js +4 -5
  3. package/bin/templates/CLAUDE.md +25 -3
  4. package/bin/templates/pwa/README.md +188 -0
  5. package/bin/templates/pwa/global.d.ts +10 -0
  6. package/bin/templates/pwa/index.html +16 -0
  7. package/bin/templates/pwa/package.json +32 -0
  8. package/bin/templates/pwa/public/icons/.gitkeep +6 -0
  9. package/bin/templates/pwa/src/daemons/notifications.ts +148 -0
  10. package/bin/templates/pwa/src/fetcher.ts +15 -0
  11. package/bin/templates/pwa/src/guards/auth.ts +12 -0
  12. package/bin/templates/pwa/src/main.ts +42 -0
  13. package/bin/templates/pwa/src/middleware/auth.ts +16 -0
  14. package/bin/templates/pwa/src/middleware/error.ts +36 -0
  15. package/bin/templates/pwa/src/middleware/retry.ts +31 -0
  16. package/bin/templates/pwa/src/pages/dashboard.ts +143 -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 +164 -0
  20. package/bin/templates/pwa/src/router.ts +20 -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/tests/helpers/test-utils.ts +84 -0
  28. package/bin/templates/pwa/tests/middleware/auth.test.ts +67 -0
  29. package/bin/templates/pwa/tests/middleware/error.test.ts +105 -0
  30. package/bin/templates/pwa/tests/middleware/retry.test.ts +103 -0
  31. package/bin/templates/pwa/tests/services/auth.test.ts +89 -0
  32. package/bin/templates/pwa/tests/services/jwt.test.ts +76 -0
  33. package/bin/templates/pwa/tests/services/storage.test.ts +69 -0
  34. package/bin/templates/{social → pwa}/tsconfig.json +11 -10
  35. package/bin/templates/pwa/vite.config.ts +94 -0
  36. package/bin/templates/{social/vite.config.ts → pwa/vitest.config.ts} +12 -17
  37. package/dist/components/music-player/snice-music-player.d.ts +72 -0
  38. package/dist/components/music-player/snice-music-player.js +730 -0
  39. package/dist/components/music-player/snice-music-player.js.map +1 -0
  40. package/dist/components/music-player/snice-music-player.types.d.ts +43 -0
  41. package/dist/components/timer/snice-timer.d.ts +27 -0
  42. package/dist/components/timer/snice-timer.js +197 -0
  43. package/dist/components/timer/snice-timer.js.map +1 -0
  44. package/dist/components/timer/snice-timer.types.d.ts +10 -0
  45. package/dist/fetcher.d.ts +65 -0
  46. package/dist/index.cjs +92 -3
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.esm.js +92 -4
  50. package/dist/index.esm.js.map +1 -1
  51. package/dist/index.iife.js +92 -3
  52. package/dist/index.iife.js.map +1 -1
  53. package/dist/symbols.cjs +1 -1
  54. package/dist/symbols.esm.js +1 -1
  55. package/dist/transitions.cjs +1 -1
  56. package/dist/transitions.esm.js +1 -1
  57. package/dist/types/context.d.ts +7 -1
  58. package/dist/types/router-options.d.ts +6 -0
  59. package/docs/ai/api.md +33 -1
  60. package/docs/ai/components/music-player.md +134 -0
  61. package/docs/ai/components/timer.md +43 -0
  62. package/docs/ai/patterns.md +48 -1
  63. package/docs/components/music-player.md +314 -0
  64. package/docs/components/timer.md +143 -0
  65. package/docs/fetcher.md +447 -0
  66. package/docs/routing.md +11 -8
  67. package/package.json +2 -1
  68. package/bin/templates/social/README.md +0 -42
  69. package/bin/templates/social/global.d.ts +0 -14
  70. package/bin/templates/social/index.html +0 -13
  71. package/bin/templates/social/package.json +0 -21
  72. package/bin/templates/social/src/main.ts +0 -33
  73. package/bin/templates/social/src/pages/feed-page.ts +0 -111
  74. package/bin/templates/social/src/pages/messages-page.ts +0 -102
  75. package/bin/templates/social/src/pages/not-found-page.ts +0 -46
  76. package/bin/templates/social/src/pages/profile-page.ts +0 -99
  77. package/bin/templates/social/src/pages/settings-page.ts +0 -119
  78. package/bin/templates/social/src/router.ts +0 -9
  79. package/bin/templates/social/src/styles/global.css +0 -156
  80. /package/bin/templates/{social → pwa}/public/vite.svg +0 -0
package/README.md CHANGED
@@ -416,10 +416,10 @@ When you modify the application context, call `update()` to notify all subscribe
416
416
  ```typescript
417
417
  @page({ tag: 'login-page', routes: ['/login'] })
418
418
  class LoginPage extends HTMLElement {
419
- private ctx?: Context<AppContext>;
419
+ private ctx?: Context;
420
420
 
421
421
  @context()
422
- handleContext(ctx: Context<AppContext>) {
422
+ handleContext(ctx: Context) {
423
423
  this.ctx = ctx;
424
424
  this.requestRender();
425
425
  }
package/bin/snice.js CHANGED
@@ -46,13 +46,12 @@ Options:
46
46
 
47
47
  Templates:
48
48
  base - Minimal starter with counter example (default)
49
- social - Social media sample app showcasing components
49
+ pwa - Progressive Web App with auth, middleware, and live notifications
50
50
 
51
51
  Examples:
52
52
  snice create-app my-app
53
- snice create-app my-app --template=social
54
- snice create-app --template=social my-app
55
- npx snice create-app my-app --template=social
53
+ snice create-app my-app --template=pwa
54
+ npx snice create-app my-app --template=pwa
56
55
  `);
57
56
  }
58
57
 
@@ -90,7 +89,7 @@ function createApp(projectPath, template = 'base') {
90
89
  // Check if template exists
91
90
  if (!existsSync(templateDir)) {
92
91
  console.error(`❌ Template "${template}" not found!`);
93
- console.error(`Available templates: base, social`);
92
+ console.error(`Available templates: base, pwa`);
94
93
  process.exit(1);
95
94
  }
96
95
 
@@ -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
- - **Pages** - Orchestrate elements, handle URLs
25
- - **Elements** - Pure presentation, no business logic
26
- - **Controllers** - Behavior, data fetching, swappable
30
+ - **Pages** - Orchestrate elements, handle URLs, most logic happens here
31
+ - **Components** - Pure presentation, no business logic
32
+ - **Controllers** - Attach to elements, add business behavior unsuitable for the page or components
33
+ - **Services** - Stateless business logic, API calls
34
+ - **Daemons** - Lifecycle-managed (WebSocket, P2P, intervals)
35
+ - **Middleware** - Composable functions (auth, retry)
36
+ - **Utils** - Pure helper functions
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, not AppContext.** Check `ctx.application.property`, not `ctx.property`. Guards: `(ctx: Context) => 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,188 @@
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
+ ### Context-Aware Fetcher
57
+
58
+ Built-in middleware system with context access:
59
+
60
+ ```typescript
61
+ // fetcher.ts - Setup
62
+ import { ContextAwareFetcher } from 'snice';
63
+
64
+ const fetcher = new ContextAwareFetcher();
65
+ fetcher.use('request', authMiddleware);
66
+ fetcher.use('response', errorMiddleware);
67
+
68
+ // Middleware with context access
69
+ export async function authMiddleware(
70
+ this: Context,
71
+ request: Request,
72
+ next: () => Promise<Response>
73
+ ): Promise<Response> {
74
+ const token = getToken();
75
+ if (token) {
76
+ request.headers.set('Authorization', `Bearer ${token}`);
77
+ }
78
+ return next();
79
+ }
80
+
81
+ // Usage in pages via ctx.fetch()
82
+ async loadData() {
83
+ const response = await this.ctx.fetch('/api/data');
84
+ const data = await response.json();
85
+ }
86
+ ```
87
+
88
+ ### Daemons for Lifecycle Management
89
+
90
+ Use daemons for resources that need start/stop/dispose:
91
+
92
+ ```typescript
93
+ const daemon = getNotificationsDaemon();
94
+ daemon.start(); // In main.ts
95
+
96
+ // In component
97
+ const unsubscribe = daemon.subscribe((notification) => {
98
+ console.log(notification);
99
+ });
100
+
101
+ // Cleanup
102
+ unsubscribe();
103
+ ```
104
+
105
+ ### Route Guards
106
+
107
+ Protect routes with guards:
108
+
109
+ ```typescript
110
+ import { authGuard } from './guards/auth';
111
+
112
+ @page({
113
+ tag: 'dashboard-page',
114
+ routes: ['/dashboard'],
115
+ guards: [authGuard]
116
+ })
117
+ export class DashboardPage extends HTMLElement {
118
+ // ...
119
+ }
120
+ ```
121
+
122
+ ### Context for Global State
123
+
124
+ Access shared state via context:
125
+
126
+ ```typescript
127
+ import type { Principal } from './types/auth';
128
+
129
+ @context()
130
+ handleContext(ctx: Context) {
131
+ const principal = ctx.application.principal as Principal | undefined;
132
+ this.user = principal?.user;
133
+ }
134
+ ```
135
+
136
+ ## Customization
137
+
138
+ ### Replace Mock API
139
+
140
+ Update `src/services/auth.ts` to call your real API:
141
+
142
+ ```typescript
143
+ export async function login(credentials: LoginCredentials) {
144
+ const response = await fetch('https://your-api.com/auth/login', {
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify(credentials)
148
+ });
149
+
150
+ const data = await response.json();
151
+ setToken(data.token);
152
+ setUser(data.user);
153
+ return data;
154
+ }
155
+ ```
156
+
157
+ ### Enable Real WebSocket
158
+
159
+ Update `src/daemons/notifications.ts`:
160
+
161
+ ```typescript
162
+ // Replace startMockNotifications() with:
163
+ this.connect();
164
+ ```
165
+
166
+ ### Configure Environment
167
+
168
+ Create `.env` file:
169
+
170
+ ```
171
+ VITE_API_URL=https://your-api.com
172
+ VITE_WS_URL=wss://your-ws.com
173
+ ```
174
+
175
+ ### Update Icons
176
+
177
+ Replace placeholder icons in `public/icons/`:
178
+ - `icon-192.png` (192x192)
179
+ - `icon-512.png` (512x512)
180
+
181
+ ## Learn More
182
+
183
+ - [Snice Documentation](https://sniceio.github.io/snice)
184
+ - [Vite PWA Plugin](https://vite-pwa-org.netlify.app/)
185
+
186
+ ## License
187
+
188
+ 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,16 @@
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="apple-touch-icon" href="/icons/icon-192.png" />
10
+ <title>{{projectName}}</title>
11
+ </head>
12
+ <body>
13
+ <div id="app"></div>
14
+ <script type="module" src="/src/main.ts"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "type-check": "tsc --noEmit",
10
+ "preview": "vite preview",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "test:coverage": "vitest run --coverage"
14
+ },
15
+ "dependencies": {
16
+ "snice": "^3.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.0.0",
20
+ "@vite-pwa/assets-generator": "^0.2.4",
21
+ "@vitest/coverage-v8": "^1.0.4",
22
+ "@vitest/ui": "^1.0.4",
23
+ "happy-dom": "^12.10.3",
24
+ "terser": "^5.24.0",
25
+ "typescript": "^5.3.3",
26
+ "unplugin-swc": "^1.5.7",
27
+ "vite": "^5.0.10",
28
+ "vite-plugin-pwa": "^0.17.4",
29
+ "vitest": "^1.0.4",
30
+ "workbox-window": "^7.0.0"
31
+ }
32
+ }
@@ -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,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,15 @@
1
+ import { ContextAwareFetcher } from 'snice';
2
+ import { authMiddleware } from './middleware/auth';
3
+ import { errorMiddleware } from './middleware/error';
4
+ import { createRetryMiddleware } from './middleware/retry';
5
+
6
+ const fetcher = new ContextAwareFetcher();
7
+
8
+ // Add request middleware (runs before fetch)
9
+ fetcher.use('request', authMiddleware);
10
+ fetcher.use('request', createRetryMiddleware());
11
+
12
+ // Add response middleware (runs after fetch)
13
+ fetcher.use('response', errorMiddleware);
14
+
15
+ export { fetcher };
@@ -0,0 +1,12 @@
1
+ import type { AppContext } from 'snice';
2
+ import type { Principal } from '../types/auth';
3
+
4
+ export function authGuard(appContext: AppContext): boolean {
5
+ const principal = appContext.principal as Principal | undefined;
6
+
7
+ if (!principal?.isAuthenticated) {
8
+ window.location.href = '#/login';
9
+ return false;
10
+ }
11
+ return true;
12
+ }
@@ -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,16 @@
1
+ import type { Context } from 'snice';
2
+ import { getToken } from '../services/storage';
3
+
4
+ export async function authMiddleware(
5
+ this: Context,
6
+ request: Request,
7
+ next: () => Promise<Response>
8
+ ): Promise<Response> {
9
+ const token = getToken();
10
+
11
+ if (token) {
12
+ request.headers.set('Authorization', `Bearer ${token}`);
13
+ }
14
+
15
+ return next();
16
+ }
@@ -0,0 +1,36 @@
1
+ import type { Context } from 'snice';
2
+ import type { Principal } from '../types/auth';
3
+ import { clearToken } from '../services/storage';
4
+
5
+ export async function errorMiddleware(
6
+ this: Context,
7
+ response: Response,
8
+ next: () => Promise<Response>
9
+ ): Promise<Response> {
10
+ // Handle 401 unauthorized - token expired or invalid
11
+ if (response.status === 401) {
12
+ clearToken();
13
+
14
+ // Update context to reflect logged out state
15
+ if (this.application.principal) {
16
+ const principal = this.application.principal as Principal;
17
+ principal.user = null;
18
+ principal.isAuthenticated = false;
19
+ }
20
+
21
+ window.location.href = '#/login';
22
+ throw new Error('Unauthorized - redirecting to login');
23
+ }
24
+
25
+ // Handle other errors
26
+ if (!response.ok) {
27
+ const contentType = response.headers.get('content-type');
28
+ if (contentType && contentType.includes('application/json')) {
29
+ const error = await response.json();
30
+ throw new Error(error.message || `Request failed with status ${response.status}`);
31
+ }
32
+ throw new Error(`Request failed with status ${response.status}`);
33
+ }
34
+
35
+ return next();
36
+ }
@@ -0,0 +1,31 @@
1
+ import type { Context } from 'snice';
2
+
3
+ export function createRetryMiddleware(retries = 3, delay = 1000) {
4
+ return async function retryMiddleware(
5
+ this: Context,
6
+ _request: Request,
7
+ next: () => Promise<Response>
8
+ ): Promise<Response> {
9
+ let lastError: Error;
10
+
11
+ for (let i = 0; i < retries; i++) {
12
+ try {
13
+ return await next();
14
+ } catch (err) {
15
+ lastError = err as Error;
16
+
17
+ // Don't retry on certain errors
18
+ if (err instanceof Error && err.message.includes('Unauthorized')) {
19
+ throw err;
20
+ }
21
+
22
+ // Wait before retrying (exponential backoff)
23
+ if (i < retries - 1) {
24
+ await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
25
+ }
26
+ }
27
+ }
28
+
29
+ throw lastError!;
30
+ };
31
+ }