@fairfox/polly 0.1.4 → 0.2.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.
@@ -0,0 +1,127 @@
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="{{PROJECT_NAME}} - A PWA with Polly" />
7
+ <link rel="manifest" href="/manifest.json" />
8
+ <title>{{PROJECT_NAME}}</title>
9
+ <style>
10
+ * {
11
+ box-sizing: border-box;
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ body {
17
+ font-family: system-ui, -apple-system, sans-serif;
18
+ padding: 20px;
19
+ max-width: 800px;
20
+ margin: 0 auto;
21
+ }
22
+
23
+ h1 {
24
+ margin-bottom: 20px;
25
+ color: #333;
26
+ }
27
+
28
+ .status {
29
+ margin-bottom: 20px;
30
+ padding: 15px;
31
+ background: #f5f5f5;
32
+ border-radius: 8px;
33
+ }
34
+
35
+ .status-item {
36
+ display: flex;
37
+ justify-content: space-between;
38
+ margin-bottom: 8px;
39
+ }
40
+
41
+ .status-label {
42
+ font-weight: 600;
43
+ }
44
+
45
+ .status-value {
46
+ color: #666;
47
+ }
48
+
49
+ .actions {
50
+ display: flex;
51
+ gap: 10px;
52
+ margin-bottom: 20px;
53
+ }
54
+
55
+ button {
56
+ padding: 10px 20px;
57
+ background: #007bff;
58
+ color: white;
59
+ border: none;
60
+ border-radius: 5px;
61
+ cursor: pointer;
62
+ font-size: 14px;
63
+ }
64
+
65
+ button:hover {
66
+ background: #0056b3;
67
+ }
68
+
69
+ button:disabled {
70
+ background: #ccc;
71
+ cursor: not-allowed;
72
+ }
73
+
74
+ #messages {
75
+ border: 1px solid #ddd;
76
+ border-radius: 8px;
77
+ padding: 15px;
78
+ min-height: 200px;
79
+ background: white;
80
+ }
81
+
82
+ .message {
83
+ padding: 8px;
84
+ margin-bottom: 8px;
85
+ background: #f9f9f9;
86
+ border-left: 3px solid #007bff;
87
+ border-radius: 4px;
88
+ font-size: 14px;
89
+ }
90
+
91
+ .message-time {
92
+ color: #666;
93
+ font-size: 12px;
94
+ margin-right: 8px;
95
+ }
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <h1>{{PROJECT_NAME}}</h1>
100
+
101
+ <div class="status">
102
+ <div class="status-item">
103
+ <span class="status-label">Service Worker:</span>
104
+ <span id="sw-status" class="status-value">Checking...</span>
105
+ </div>
106
+ <div class="status-item">
107
+ <span class="status-label">Shared Worker:</span>
108
+ <span id="shared-worker-status" class="status-value">Checking...</span>
109
+ </div>
110
+ <div class="status-item">
111
+ <span class="status-label">Messages Received:</span>
112
+ <span id="message-count" class="status-value">0</span>
113
+ </div>
114
+ </div>
115
+
116
+ <div class="actions">
117
+ <button id="ping-sw">Ping Service Worker</button>
118
+ <button id="ping-shared">Ping Shared Worker</button>
119
+ <button id="broadcast">Broadcast Message</button>
120
+ </div>
121
+
122
+ <h2>Messages</h2>
123
+ <div id="messages"></div>
124
+
125
+ <script type="module" src="/src/main.ts"></script>
126
+ </body>
127
+ </html>
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "bun run --hot src/main.ts",
7
+ "build": "bun build.ts",
8
+ "serve": "bun --hot server.ts",
9
+ "typecheck": "bun tsc --noEmit",
10
+ "visualize": "bun ../../visualize/src/cli.ts"
11
+ },
12
+ "dependencies": {
13
+ "@fairfox/polly": "*"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.0.0",
17
+ "@types/bun": "latest"
18
+ }
19
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "short_name": "{{PROJECT_NAME}}",
4
+ "description": "A PWA built with Polly framework",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#ffffff",
8
+ "theme_color": "#000000",
9
+ "icons": [
10
+ {
11
+ "src": "/icon-192.png",
12
+ "sizes": "192x192",
13
+ "type": "image/png"
14
+ },
15
+ {
16
+ "src": "/icon-512.png",
17
+ "sizes": "512x512",
18
+ "type": "image/png"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Development server for PWA
3
+ * Serves the application with hot reload support
4
+ */
5
+
6
+ const port = 3000
7
+
8
+ const server = Bun.serve({
9
+ port,
10
+ async fetch(req) {
11
+ const url = new URL(req.url)
12
+ let filePath = url.pathname
13
+
14
+ // Serve root
15
+ if (filePath === '/') {
16
+ filePath = '/index.html'
17
+ }
18
+
19
+ // Try to serve from public directory first
20
+ const publicFile = Bun.file(`./public${filePath}`)
21
+ if (await publicFile.exists()) {
22
+ return new Response(publicFile)
23
+ }
24
+
25
+ // Try to serve from root
26
+ const rootFile = Bun.file(`.${filePath}`)
27
+ if (await rootFile.exists()) {
28
+ return new Response(rootFile)
29
+ }
30
+
31
+ // Handle TypeScript files - transpile on the fly
32
+ if (filePath.endsWith('.ts')) {
33
+ const tsFile = Bun.file(`.${filePath}`)
34
+ if (await tsFile.exists()) {
35
+ const transpiled = await Bun.build({
36
+ entrypoints: [`.${filePath}`],
37
+ format: 'esm',
38
+ target: 'browser',
39
+ })
40
+
41
+ if (transpiled.outputs.length > 0) {
42
+ return new Response(transpiled.outputs[0], {
43
+ headers: {
44
+ 'Content-Type': 'application/javascript',
45
+ },
46
+ })
47
+ }
48
+ }
49
+ }
50
+
51
+ // 404
52
+ return new Response('Not Found', { status: 404 })
53
+ },
54
+ })
55
+
56
+ console.log(`🚀 Server running at http://localhost:${port}`)
57
+ console.log(`\n📱 PWA: {{PROJECT_NAME}}`)
58
+ console.log(`\n💡 Open in browser and check the console for worker messages`)
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Main Application Context
3
+ * Coordinates between Service Worker and Shared Worker
4
+ */
5
+
6
+ // Register Service Worker
7
+ if ('serviceWorker' in navigator) {
8
+ window.addEventListener('load', async () => {
9
+ try {
10
+ const registration = await navigator.serviceWorker.register('/src/service-worker.ts', {
11
+ type: 'module'
12
+ })
13
+ console.log('Service Worker registered:', registration.scope)
14
+ updateStatus('sw-status', '✓ Active')
15
+ } catch (error) {
16
+ console.error('Service Worker registration failed:', error)
17
+ updateStatus('sw-status', '✗ Failed')
18
+ }
19
+ })
20
+
21
+ // Listen for messages from Service Worker
22
+ navigator.serviceWorker.addEventListener('message', (event) => {
23
+ console.log('Message from Service Worker:', event.data)
24
+ addMessage(`[SW] ${JSON.stringify(event.data)}`)
25
+ })
26
+ }
27
+
28
+ // Initialize Shared Worker
29
+ let sharedWorker: SharedWorker | null = null
30
+ let messageCount = 0
31
+
32
+ if (typeof SharedWorker !== 'undefined') {
33
+ sharedWorker = new SharedWorker(
34
+ new URL('./shared-worker.ts', import.meta.url),
35
+ { type: 'module', name: 'polly-shared-worker' }
36
+ )
37
+
38
+ sharedWorker.port.start()
39
+
40
+ // Listen for messages from Shared Worker
41
+ sharedWorker.port.addEventListener('message', (event) => {
42
+ console.log('Message from Shared Worker:', event.data)
43
+ addMessage(`[Shared] ${JSON.stringify(event.data)}`)
44
+ messageCount++
45
+ updateMessageCount()
46
+ })
47
+
48
+ updateStatus('shared-worker-status', '✓ Connected')
49
+ } else {
50
+ updateStatus('shared-worker-status', '✗ Not Supported')
51
+ }
52
+
53
+ // UI Event Handlers
54
+ document.getElementById('ping-sw')?.addEventListener('click', async () => {
55
+ if (navigator.serviceWorker.controller) {
56
+ navigator.serviceWorker.controller.postMessage({
57
+ type: 'PING',
58
+ timestamp: Date.now()
59
+ })
60
+ addMessage('[Main] Sent PING to Service Worker')
61
+ } else {
62
+ addMessage('[Main] Service Worker not active')
63
+ }
64
+ })
65
+
66
+ document.getElementById('ping-shared')?.addEventListener('click', () => {
67
+ if (sharedWorker) {
68
+ sharedWorker.port.postMessage({
69
+ type: 'PING',
70
+ timestamp: Date.now()
71
+ })
72
+ addMessage('[Main] Sent PING to Shared Worker')
73
+ } else {
74
+ addMessage('[Main] Shared Worker not available')
75
+ }
76
+ })
77
+
78
+ document.getElementById('broadcast')?.addEventListener('click', () => {
79
+ const message = {
80
+ type: 'BROADCAST',
81
+ data: 'Hello from main context',
82
+ timestamp: Date.now()
83
+ }
84
+
85
+ // Send to both workers
86
+ if (navigator.serviceWorker.controller) {
87
+ navigator.serviceWorker.controller.postMessage(message)
88
+ }
89
+
90
+ if (sharedWorker) {
91
+ sharedWorker.port.postMessage(message)
92
+ }
93
+
94
+ addMessage('[Main] Broadcast sent to all workers')
95
+ })
96
+
97
+ // UI Helper Functions
98
+ function updateStatus(elementId: string, status: string) {
99
+ const element = document.getElementById(elementId)
100
+ if (element) {
101
+ element.textContent = status
102
+ }
103
+ }
104
+
105
+ function updateMessageCount() {
106
+ const element = document.getElementById('message-count')
107
+ if (element) {
108
+ element.textContent = messageCount.toString()
109
+ }
110
+ }
111
+
112
+ function addMessage(text: string) {
113
+ const messagesDiv = document.getElementById('messages')
114
+ if (!messagesDiv) return
115
+
116
+ const messageDiv = document.createElement('div')
117
+ messageDiv.className = 'message'
118
+
119
+ const time = new Date().toLocaleTimeString()
120
+ messageDiv.innerHTML = `<span class="message-time">${time}</span>${text}`
121
+
122
+ messagesDiv.insertBefore(messageDiv, messagesDiv.firstChild)
123
+
124
+ // Keep only last 20 messages
125
+ while (messagesDiv.children.length > 20) {
126
+ messagesDiv.removeChild(messagesDiv.lastChild!)
127
+ }
128
+ }
129
+
130
+ // Initial message
131
+ addMessage('[Main] Application initialized')
132
+
133
+ console.log('{{PROJECT_NAME}} initialized')
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Service Worker Context
3
+ * Handles background tasks, caching, and push notifications
4
+ */
5
+
6
+ /// <reference lib="WebWorker" />
7
+ declare const self: ServiceWorkerGlobalScope
8
+
9
+ console.log('[SW] Service Worker loading...')
10
+
11
+ // Cache configuration
12
+ const CACHE_NAME = '{{PROJECT_NAME}}-v1'
13
+ const urlsToCache = [
14
+ '/',
15
+ '/index.html',
16
+ '/src/main.ts'
17
+ ]
18
+
19
+ // Install event - cache resources
20
+ self.addEventListener('install', (event) => {
21
+ console.log('[SW] Installing...')
22
+
23
+ event.waitUntil(
24
+ caches.open(CACHE_NAME).then((cache) => {
25
+ console.log('[SW] Caching resources')
26
+ return cache.addAll(urlsToCache)
27
+ })
28
+ )
29
+
30
+ // Activate immediately
31
+ self.skipWaiting()
32
+ })
33
+
34
+ // Activate event - clean up old caches
35
+ self.addEventListener('activate', (event) => {
36
+ console.log('[SW] Activating...')
37
+
38
+ event.waitUntil(
39
+ caches.keys().then((cacheNames) => {
40
+ return Promise.all(
41
+ cacheNames
42
+ .filter((name) => name !== CACHE_NAME)
43
+ .map((name) => caches.delete(name))
44
+ )
45
+ })
46
+ )
47
+
48
+ // Take control immediately
49
+ return self.clients.claim()
50
+ })
51
+
52
+ // Fetch event - serve from cache, fallback to network
53
+ self.addEventListener('fetch', (event) => {
54
+ event.respondWith(
55
+ caches.match(event.request).then((response) => {
56
+ // Cache hit - return response
57
+ if (response) {
58
+ return response
59
+ }
60
+
61
+ // Clone the request
62
+ const fetchRequest = event.request.clone()
63
+
64
+ return fetch(fetchRequest).then((response) => {
65
+ // Check if valid response
66
+ if (!response || response.status !== 200 || response.type !== 'basic') {
67
+ return response
68
+ }
69
+
70
+ // Clone the response
71
+ const responseToCache = response.clone()
72
+
73
+ caches.open(CACHE_NAME).then((cache) => {
74
+ cache.put(event.request, responseToCache)
75
+ })
76
+
77
+ return response
78
+ })
79
+ })
80
+ )
81
+ })
82
+
83
+ // Message handling
84
+ self.addEventListener('message', (event) => {
85
+ console.log('[SW] Received message:', event.data)
86
+
87
+ switch (event.data.type) {
88
+ case 'PING':
89
+ // Respond to ping
90
+ event.ports[0]?.postMessage({
91
+ type: 'PONG',
92
+ timestamp: Date.now(),
93
+ originalTimestamp: event.data.timestamp
94
+ })
95
+
96
+ // Also broadcast to all clients
97
+ self.clients.matchAll().then((clients) => {
98
+ clients.forEach((client) => {
99
+ client.postMessage({
100
+ type: 'PONG',
101
+ from: 'service-worker',
102
+ timestamp: Date.now()
103
+ })
104
+ })
105
+ })
106
+ break
107
+
108
+ case 'BROADCAST':
109
+ // Broadcast to all clients
110
+ self.clients.matchAll().then((clients) => {
111
+ console.log(`[SW] Broadcasting to ${clients.length} clients`)
112
+ clients.forEach((client) => {
113
+ client.postMessage({
114
+ type: 'BROADCAST',
115
+ data: event.data.data,
116
+ from: 'service-worker',
117
+ timestamp: Date.now()
118
+ })
119
+ })
120
+ })
121
+ break
122
+
123
+ case 'SKIP_WAITING':
124
+ self.skipWaiting()
125
+ break
126
+
127
+ default:
128
+ console.log('[SW] Unknown message type:', event.data.type)
129
+ }
130
+ })
131
+
132
+ // Push notification event (example)
133
+ self.addEventListener('push', (event) => {
134
+ console.log('[SW] Push received')
135
+
136
+ const data = event.data?.json() ?? {}
137
+ const title = data.title || '{{PROJECT_NAME}}'
138
+ const options = {
139
+ body: data.body || 'New notification',
140
+ icon: '/icon-192.png',
141
+ badge: '/icon-192.png',
142
+ data: data
143
+ }
144
+
145
+ event.waitUntil(
146
+ self.registration.showNotification(title, options)
147
+ )
148
+ })
149
+
150
+ // Notification click event
151
+ self.addEventListener('notificationclick', (event) => {
152
+ console.log('[SW] Notification clicked')
153
+
154
+ event.notification.close()
155
+
156
+ event.waitUntil(
157
+ self.clients.openWindow('/')
158
+ )
159
+ })
160
+
161
+ console.log('[SW] Service Worker loaded')
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Shared Worker Context
3
+ * Shared state and coordination across multiple tabs/windows
4
+ */
5
+
6
+ /// <reference lib="WebWorker" />
7
+ declare const self: SharedWorkerGlobalScope
8
+
9
+ console.log('[Shared Worker] Starting...')
10
+
11
+ // Track connected ports (tabs/windows)
12
+ const ports: MessagePort[] = []
13
+ let messageCount = 0
14
+
15
+ // Shared state across all tabs
16
+ interface SharedState {
17
+ connectedTabs: number
18
+ totalMessages: number
19
+ lastActivity: number
20
+ }
21
+
22
+ const state: SharedState = {
23
+ connectedTabs: 0,
24
+ totalMessages: 0,
25
+ lastActivity: Date.now()
26
+ }
27
+
28
+ // Connection event - when a new tab connects
29
+ self.addEventListener('connect', (event) => {
30
+ const port = event.ports[0]
31
+ ports.push(port)
32
+ state.connectedTabs = ports.length
33
+
34
+ console.log(`[Shared Worker] New connection. Total ports: ${ports.length}`)
35
+
36
+ // Send welcome message
37
+ port.postMessage({
38
+ type: 'CONNECTED',
39
+ state: state,
40
+ timestamp: Date.now()
41
+ })
42
+
43
+ // Listen for messages from this port
44
+ port.addEventListener('message', (messageEvent) => {
45
+ handleMessage(messageEvent.data, port)
46
+ })
47
+
48
+ // Start listening
49
+ port.start()
50
+
51
+ // Notify all tabs about new connection
52
+ broadcast({
53
+ type: 'TAB_CONNECTED',
54
+ connectedTabs: state.connectedTabs
55
+ })
56
+ })
57
+
58
+ function handleMessage(data: any, sourcePort: MessagePort) {
59
+ console.log('[Shared Worker] Received:', data)
60
+
61
+ state.totalMessages++
62
+ state.lastActivity = Date.now()
63
+
64
+ switch (data.type) {
65
+ case 'PING':
66
+ // Respond to ping
67
+ sourcePort.postMessage({
68
+ type: 'PONG',
69
+ timestamp: Date.now(),
70
+ originalTimestamp: data.timestamp,
71
+ state: state
72
+ })
73
+ break
74
+
75
+ case 'BROADCAST':
76
+ // Broadcast to all connected tabs
77
+ broadcast({
78
+ type: 'BROADCAST',
79
+ data: data.data,
80
+ from: 'shared-worker',
81
+ timestamp: Date.now()
82
+ })
83
+ break
84
+
85
+ case 'GET_STATE':
86
+ // Send current state
87
+ sourcePort.postMessage({
88
+ type: 'STATE',
89
+ state: state,
90
+ timestamp: Date.now()
91
+ })
92
+ break
93
+
94
+ case 'INCREMENT_COUNTER':
95
+ // Example of shared state manipulation
96
+ messageCount++
97
+ broadcast({
98
+ type: 'COUNTER_UPDATED',
99
+ count: messageCount,
100
+ timestamp: Date.now()
101
+ })
102
+ break
103
+
104
+ default:
105
+ console.log('[Shared Worker] Unknown message type:', data.type)
106
+ sourcePort.postMessage({
107
+ type: 'ERROR',
108
+ message: `Unknown message type: ${data.type}`
109
+ })
110
+ }
111
+ }
112
+
113
+ // Broadcast message to all connected tabs
114
+ function broadcast(message: any) {
115
+ console.log(`[Shared Worker] Broadcasting to ${ports.length} ports:`, message)
116
+
117
+ ports.forEach((port) => {
118
+ try {
119
+ port.postMessage(message)
120
+ } catch (error) {
121
+ console.error('[Shared Worker] Failed to send message to port:', error)
122
+ }
123
+ })
124
+ }
125
+
126
+ // Periodic heartbeat to all tabs
127
+ setInterval(() => {
128
+ broadcast({
129
+ type: 'HEARTBEAT',
130
+ state: state,
131
+ timestamp: Date.now()
132
+ })
133
+ }, 30000) // Every 30 seconds
134
+
135
+ console.log('[Shared Worker] Ready')
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"],
6
+ "moduleResolution": "bundler",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }