a demonstration replicated social networking web app built with anproto
wiredove.net/
social
ed25519
protocols
1self.addEventListener('install', (event) => {
2 event.waitUntil((async () => {
3 const cache = await caches.open(APP_SHELL_CACHE)
4 await cache.addAll(APP_SHELL_ASSETS)
5 await self.skipWaiting()
6 })())
7})
8
9self.addEventListener('activate', (event) => {
10 event.waitUntil((async () => {
11 const keys = await caches.keys()
12 await Promise.all(
13 keys
14 .filter((key) => key.startsWith('wiredove-') && key !== APP_SHELL_CACHE)
15 .map((key) => caches.delete(key))
16 )
17 await self.clients.claim()
18 })())
19})
20
21const CACHE_VERSION = 'v1'
22const APP_SHELL_CACHE = `wiredove-app-shell-${CACHE_VERSION}`
23const APP_SHELL_ASSETS = [
24 '/',
25 '/index.html',
26 '/style.css',
27 '/app.js',
28 '/manifest.json',
29 '/favicon.ico',
30 '/dovepurple_sm.png',
31 '/dovepurple.png',
32 '/dove_sm.png',
33]
34
35const isCacheableRequest = (request) => {
36 if (!request || request.method !== 'GET') { return false }
37 const url = new URL(request.url)
38 if (url.origin !== self.location.origin) { return false }
39 return true
40}
41
42const isNavigationRequest = (request) => request.mode === 'navigate'
43
44const isStaticAssetRequest = (request) => {
45 const url = new URL(request.url)
46 return /\.(?:js|css|png|jpg|jpeg|svg|ico|webmanifest|json)$/i.test(url.pathname)
47}
48
49const canCacheResponse = (response) => Boolean(response && response.ok)
50
51const staleWhileRevalidate = async (request) => {
52 const cache = await caches.open(APP_SHELL_CACHE)
53 const cached = await cache.match(request)
54 const networkPromise = fetch(request).then(async (response) => {
55 if (canCacheResponse(response)) {
56 await cache.put(request, response.clone())
57 }
58 return response
59 }).catch(() => null)
60 if (cached) {
61 void networkPromise
62 return cached
63 }
64 const fresh = await networkPromise
65 if (fresh) { return fresh }
66 return Response.error()
67}
68
69const networkFirst = async (request) => {
70 const cache = await caches.open(APP_SHELL_CACHE)
71 try {
72 const response = await fetch(request)
73 if (canCacheResponse(response)) {
74 await cache.put(request, response.clone())
75 }
76 return response
77 } catch {
78 const cached = await cache.match(request)
79 if (cached) { return cached }
80 if (isNavigationRequest(request)) {
81 const fallback = await cache.match('/index.html')
82 if (fallback) { return fallback }
83 }
84 return Response.error()
85 }
86}
87
88self.addEventListener('fetch', (event) => {
89 const { request } = event
90 if (!isCacheableRequest(request)) { return }
91 if (isNavigationRequest(request)) {
92 event.respondWith(networkFirst(request))
93 return
94 }
95 if (isStaticAssetRequest(request)) {
96 event.respondWith(staleWhileRevalidate(request))
97 }
98})
99
100self.addEventListener('push', (event) => {
101 let payload = { title: 'wiredove', body: 'New message', url: '/' }
102 if (event.data) {
103 try {
104 payload = event.data.json()
105 } catch {
106 try {
107 payload = JSON.parse(event.data.text())
108 } catch {
109 payload.body = event.data.text()
110 }
111 }
112 }
113
114 const hash = payload.hash
115 const targetUrl = payload.url ||
116 (typeof hash === 'string' && hash.length > 0
117 ? `https://wiredove.net/#${hash}`
118 : '/')
119 const options = {
120 body: payload.body,
121 data: { url: targetUrl },
122 icon: payload.icon || '/dovepurple_sm.png',
123 badge: payload.badge,
124 }
125
126 event.waitUntil(self.registration.showNotification(payload.title, options))
127})
128
129self.addEventListener('notificationclick', (event) => {
130 event.notification.close()
131 const target = event.notification.data?.url || '/'
132
133 event.waitUntil(
134 self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
135 for (const client of clients) {
136 if (client.url.includes(target) && 'focus' in client) return client.focus()
137 }
138 if (self.clients.openWindow) return self.clients.openWindow(target)
139 return undefined
140 }),
141 )
142})