156 lines
5.7 KiB
JavaScript
156 lines
5.7 KiB
JavaScript
const BASE = new URL('./', self.location.href).pathname;
|
|
importScripts(BASE + 'static/background-issue-sync.js');
|
|
const CACHE = 'stackchain-dashboard-shell-v26';
|
|
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
|
const SHELL = [
|
|
BASE,
|
|
BASE + 'manifest.webmanifest',
|
|
BASE + 'static/dashboard.css',
|
|
BASE + 'static/dashboard.js',
|
|
BASE + 'static/icons/stackchain-192.png',
|
|
BASE + 'static/icons/stackchain-512.png',
|
|
BASE + 'static/session.js',
|
|
BASE + 'static/markdown.js',
|
|
BASE + 'static/commands.js',
|
|
BASE + 'static/search-preview.js',
|
|
BASE + 'static/widgets.js',
|
|
BASE + 'static/drafts.js',
|
|
BASE + 'static/outbox-coordinator.js',
|
|
BASE + 'static/issue-outbox.js',
|
|
BASE + 'static/authored-outbox.js',
|
|
BASE + 'static/offline-work.js',
|
|
BASE + 'static/my-work.js',
|
|
BASE + 'static/today-work.js',
|
|
BASE + 'static/later-work.js',
|
|
BASE + 'static/detail-defer.js',
|
|
BASE + 'static/pick-work.js',
|
|
BASE + 'static/conversation.js',
|
|
BASE + 'static/issue-sheet.js',
|
|
BASE + 'static/create-issue-sheet.js',
|
|
BASE + 'static/pull-sheet.js',
|
|
BASE + 'static/review-sheet.js',
|
|
BASE + 'static/work-route.js',
|
|
BASE + 'static/context-poller.js',
|
|
BASE + 'static/mobile-task-dock.js',
|
|
BASE + 'static/install-app.js',
|
|
BASE + 'static/background-issue-sync.js',
|
|
];
|
|
|
|
async function sessionCsrf() {
|
|
const response = await fetch(new URL(BASE + 'api/v1/session', self.location.origin), {
|
|
headers: { Accept: 'application/json' },
|
|
});
|
|
if (!response.ok) return '';
|
|
const payload = await response.json().catch(() => ({}));
|
|
return typeof payload.csrf_token === 'string' ? payload.csrf_token : '';
|
|
}
|
|
|
|
async function fetchJson(url, options = {}) {
|
|
const requestOptions = { ...options };
|
|
const method = String(options.method || 'GET').toUpperCase();
|
|
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
|
const headers = new Headers(options.headers || {});
|
|
const csrf = await sessionCsrf();
|
|
if (csrf) headers.set('X-CSRF-Token', csrf);
|
|
requestOptions.headers = headers;
|
|
}
|
|
const response = await fetch(new URL(url, self.location.origin), requestOptions);
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
const error = new Error(payload.error || payload.detail || 'Background issue delivery failed.');
|
|
error.status = response.status;
|
|
throw error;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
const issueSync = self.__issueSync || createBackgroundIssueSync({
|
|
store: createIssueSyncStore(), fetchJson, base: BASE,
|
|
});
|
|
|
|
async function flushAndNotify() {
|
|
const result = await issueSync.flush();
|
|
if (!result?.login || !result.receipts?.length ||
|
|
!await issueSync.getReceiptPreference?.(result.login)) return;
|
|
for (const receipt of result.receipts) {
|
|
const needsAttention = receipt.status === 'attention';
|
|
const title = needsAttention
|
|
? 'Queued work needs attention'
|
|
: receipt.kind === 'issue' ? 'Queued issue created' : 'Queued message sent';
|
|
await self.registration.showNotification(title, {
|
|
body: needsAttention ? 'Tap to review it in Drafts.' : 'Tap to open it in Stackchain.',
|
|
tag: 'stackchain-delivery-' + receipt.id,
|
|
data: { route: receipt.route },
|
|
});
|
|
}
|
|
}
|
|
|
|
self.addEventListener('install', event => {
|
|
event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(SHELL)).then(() => self.skipWaiting()));
|
|
});
|
|
|
|
self.addEventListener('activate', event => {
|
|
event.waitUntil(caches.keys().then(keys => Promise.all(
|
|
keys.filter(key => key.startsWith('stackchain-dashboard-') && key !== CACHE)
|
|
.map(key => caches.delete(key))
|
|
)).then(() => self.clients.claim()));
|
|
});
|
|
|
|
self.addEventListener('sync', event => {
|
|
if (event.tag === 'stackchain-issue-outbox-v1') event.waitUntil(flushAndNotify());
|
|
});
|
|
|
|
self.addEventListener('message', event => {
|
|
if (event.data?.type === 'stackchain-resume-outbox') event.waitUntil(flushAndNotify());
|
|
if (event.data?.type === 'stackchain-purge-outbox') event.waitUntil((async () => {
|
|
try {
|
|
await issueSync.purge();
|
|
event.ports?.[0]?.postMessage({ ok: true });
|
|
} catch (error) {
|
|
event.ports?.[0]?.postMessage({ ok: false, error: String(error?.message || 'Outbox purge failed.') });
|
|
}
|
|
})());
|
|
});
|
|
|
|
self.addEventListener('notificationclick', event => {
|
|
event.notification.close();
|
|
const route = String(event.notification.data?.route || '');
|
|
if (!route.startsWith('#/my-work/')) return;
|
|
const target = new URL(BASE + route, self.location.origin).href;
|
|
event.waitUntil((async () => {
|
|
const windows = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
|
|
const client = windows.find(candidate => candidate.url.startsWith(self.location.origin + BASE));
|
|
if (!client) return self.clients.openWindow(target);
|
|
if (client.navigate) await client.navigate(target);
|
|
return client.focus();
|
|
})());
|
|
});
|
|
|
|
self.addEventListener('fetch', event => {
|
|
const request = event.request;
|
|
if (request.method !== 'GET' || request.url.includes('/api/')) return;
|
|
const url = new URL(request.url);
|
|
if (url.origin !== self.location.origin || !url.pathname.startsWith(BASE)) return;
|
|
if (request.mode === 'navigate') {
|
|
event.respondWith(
|
|
fetch(request).then(async response => {
|
|
const cache = await caches.open(CACHE);
|
|
if (response.ok) {
|
|
await cache.put(BASE, response.clone());
|
|
} else if (OUTAGE_STATUSES.has(response.status)) {
|
|
const cached = await cache.match(BASE);
|
|
if (cached) return cached;
|
|
}
|
|
return response;
|
|
}).catch(async () => {
|
|
const cache = await caches.open(CACHE);
|
|
return cache.match(BASE);
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
if (url.origin === self.location.origin && SHELL.includes(url.pathname)) {
|
|
event.respondWith(caches.match(request).then(cached => cached || fetch(request)));
|
|
}
|
|
});
|