@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.
- package/README.md +560 -184
- package/dist/cli/polly.js +75 -220
- package/dist/cli/polly.js.map +5 -4
- package/dist/cli/template-utils.js +81 -0
- package/dist/cli/template-utils.js.map +10 -0
- package/dist/vendor/verify/src/cli.js +1 -16
- package/dist/vendor/verify/src/cli.js.map +2 -2
- package/dist/vendor/visualize/src/cli.js +1 -16
- package/dist/vendor/visualize/src/cli.js.map +2 -2
- package/package.json +2 -2
- package/templates/pwa/.gitignore.template +4 -0
- package/templates/pwa/README.md.template +144 -0
- package/templates/pwa/build.ts.template +56 -0
- package/templates/pwa/index.html.template +127 -0
- package/templates/pwa/package.json.template +19 -0
- package/templates/pwa/public/manifest.json.template +21 -0
- package/templates/pwa/server.ts.template +58 -0
- package/templates/pwa/src/main.ts.template +133 -0
- package/templates/pwa/src/service-worker.ts.template +161 -0
- package/templates/pwa/src/shared-worker.ts.template +135 -0
- package/templates/pwa/tsconfig.json.template +18 -0
|
@@ -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
|
+
}
|