PageSourceSearch

https://radineer.asia/sw.js

js radineer.asia collected 2026-10-04 07:52:43 UTC 4,965 bytes, 187 lines download raw bytes

1// Service Worker for Radineer PWA
2const CACHE_NAME = 'radineer-v1';
3const OFFLINE_URL = '/offline';
4
5// キャッシュするリソース
6const STATIC_ASSETS = [
7  '/',
8  '/offline',
9  '/manifest.json',
10];
11
12// インストール時にキャッシュ
13self.addEventListener('install', (event) => {
14  event.waitUntil(
15    caches.open(CACHE_NAME).then((cache) => {
16      console.log('Service Worker: Caching static assets');
17      return cache.addAll(STATIC_ASSETS);
18    })
19  );
20  // 即座にアクティベート
21  self.skipWaiting();
22});
23
24// アクティベート時に古いキャッシュを削除
25self.addEventListener('activate', (event) => {
26  event.waitUntil(
27    caches.keys().then((cacheNames) => {
28      return Promise.all(
29        cacheNames
30          .filter((name) => name !== CACHE_NAME)
31          .map((name) => {
32            console.log('Service Worker: Deleting old cache:', name);
33            return caches.delete(name);
34          })
35      );
36    })
37  );
38  // 全てのクライアントを制御
39  self.clients.claim();
40});
41
42// フェッチリクエストの処理
43self.addEventListener('fetch', (event) => {
44  const { request } = event;
45  const url = new URL(request.url);
46
47  // 同じオリジンのリクエストのみ処理
48  if (url.origin !== location.origin) {
49    return;
50  }
51
52  // APIリクエストはネットワーク優先
53  if (url.pathname.startsWith('/api/')) {
54    event.respondWith(networkFirst(request));
55    return;
56  }
57
58  // ナビゲーションリクエスト(HTML)
59  if (request.mode === 'navigate') {
60    event.respondWith(
61      fetch(request)
62        .then((response) => {
63          // 成功したレスポンスをキャッシュ
64          if (response.ok) {
65            const responseClone = response.clone();
66            caches.open(CACHE_NAME).then((cache) => {
67              cache.put(request, responseClone);
68            });
69          }
70          return response;
71        })
72        .catch(() => {
73          // オフライン時はキャッシュから
74          return caches.match(request).then((cachedResponse) => {
75            return cachedResponse || caches.match(OFFLINE_URL);
76          });
77        })
78    );
79    return;
80  }
81
82  // 静的アセットはキャッシュ優先
83  if (isStaticAsset(url.pathname)) {
84    event.respondWith(cacheFirst(request));
85    return;
86  }
87
88  // その他はネットワーク優先
89  event.respondWith(networkFirst(request));
90});
91
92// キャッシュ優先戦略
93async function cacheFirst(request) {
94  const cachedResponse = await caches.match(request);
95  if (cachedResponse) {
96    return cachedResponse;
97  }
98
99  try {
100    const networkResponse = await fetch(request);
101    if (networkResponse.ok) {
102      const cache = await caches.open(CACHE_NAME);
103      cache.put(request, networkResponse.clone());
104    }
105    return networkResponse;
106  } catch (error) {
107    console.error('Service Worker: Fetch failed:', error);
108    throw error;
109  }
110}
111
112// ネットワーク優先戦略
113async function networkFirst(request) {
114  try {
115    const networkResponse = await fetch(request);
116    if (networkResponse.ok) {
117      const cache = await caches.open(CACHE_NAME);
118      cache.put(request, networkResponse.clone());
119    }
120    return networkResponse;
121  } catch (error) {
122    const cachedResponse = await caches.match(request);
123    if (cachedResponse) {
124      return cachedResponse;
125    }
126    throw error;
127  }
128}
129
130// 静的アセットかどうかを判定
131function isStaticAsset(pathname) {
132  return (
133    pathname.startsWith('/_next/static/') ||
134    pathname.startsWith('/images/') ||
135    pathname.startsWith('/icons/') ||
136    pathname.endsWith('.js') ||
137    pathname.endsWith('.css') ||
138    pathname.endsWith('.png') ||
139    pathname.endsWith('.jpg') ||
140    pathname.endsWith('.jpeg') ||
141    pathname.endsWith('.svg') ||
142    pathname.endsWith('.webp') ||
143    pathname.endsWith('.woff2') ||
144    pathname.endsWith('.woff')
145  );
146}
147
148// プッシュ通知(将来の拡張用)
149self.addEventListener('push', (event) => {
150  if (!event.data) return;
151
152  const data = event.data.json();
153  const options = {
154    body: data.body,
155    icon: '/icons/icon-192x192.png',
156    badge: '/icons/icon-72x72.png',
157    vibrate: [100, 50, 100],
158    data: {
159      url: data.url || '/',
160    },
161  };
162
163  event.waitUntil(
164    self.registration.showNotification(data.title || 'Radineer', options)
165  );
166});
167
168// 通知クリック
169self.addEventListener('notificationclick', (event) => {
170  event.notification.close();
171
172  const url = event.notification.data?.url || '/';
173  event.waitUntil(
174    clients.matchAll({ type: 'window' }).then((windowClients) => {
175      // 既存のウィンドウがあればフォーカス
176      for (const client of windowClients) {
177        if (client.url === url && 'focus' in client) {
178          return client.focus();
179        }
180      }
181      // なければ新しいウィンドウを開く
182      if (clients.openWindow) {
183        return clients.openWindow(url);
184      }
185    })
186  );
187});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.