@affectively/aeon-pages 1.3.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/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aeon Service Worker - Total Preload Strategy
|
|
3
|
+
*
|
|
4
|
+
* The Aeon architecture is recursive:
|
|
5
|
+
* - This service worker caches the ENTIRE site as an Aeon
|
|
6
|
+
* - Each page session is an Aeon entity within the site Aeon
|
|
7
|
+
* - Federation would cache multiple sites as Aeons of Aeons
|
|
8
|
+
*
|
|
9
|
+
* With 8.4KB framework + ~2-5KB per page session, we can preload EVERYTHING.
|
|
10
|
+
* A site with 100 pages = ~315KB total (smaller than one hero image!)
|
|
11
|
+
*
|
|
12
|
+
* Optional Push & Offline Features:
|
|
13
|
+
* - Push notification handling (when enabled)
|
|
14
|
+
* - Background sync for offline queue
|
|
15
|
+
* - Notification click/close handlers
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/// <reference lib="webworker" />
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
handlePush,
|
|
22
|
+
handleNotificationClick,
|
|
23
|
+
handleNotificationClose,
|
|
24
|
+
handleSync,
|
|
25
|
+
type PushHandlerConfig,
|
|
26
|
+
} from './service-worker-push';
|
|
27
|
+
|
|
28
|
+
declare const self: ServiceWorkerGlobalScope;
|
|
29
|
+
|
|
30
|
+
const CACHE_NAME = 'aeon-v1';
|
|
31
|
+
const MANIFEST_URL = '/.aeon/manifest.json';
|
|
32
|
+
const SESSIONS_PREFIX = '/.aeon/sessions/';
|
|
33
|
+
|
|
34
|
+
interface RouteManifest {
|
|
35
|
+
version: string;
|
|
36
|
+
routes: Array<{
|
|
37
|
+
pattern: string;
|
|
38
|
+
sessionId: string;
|
|
39
|
+
isAeon: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
generatedAt: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CacheMessage {
|
|
45
|
+
type:
|
|
46
|
+
| 'CACHE_STATUS'
|
|
47
|
+
| 'PRELOAD_PROGRESS'
|
|
48
|
+
| 'PRELOAD_COMPLETE'
|
|
49
|
+
| 'QUEUE_STATUS'
|
|
50
|
+
| 'SYNC_OFFLINE_QUEUE';
|
|
51
|
+
loaded?: number;
|
|
52
|
+
total?: number;
|
|
53
|
+
percentage?: number;
|
|
54
|
+
cachedRoutes?: string[];
|
|
55
|
+
queueStats?: {
|
|
56
|
+
pending: number;
|
|
57
|
+
syncing: number;
|
|
58
|
+
failed: number;
|
|
59
|
+
totalBytes: number;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Push notification configuration
|
|
65
|
+
* Set via AEON_PUSH_CONFIG in service worker scope
|
|
66
|
+
*/
|
|
67
|
+
const pushConfig: PushHandlerConfig = {
|
|
68
|
+
defaultIcon: '/icon-192.png',
|
|
69
|
+
defaultBadge: '/badge-72.png',
|
|
70
|
+
defaultVibrate: [200, 100, 200],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Install event - Pre-cache the framework and manifest
|
|
75
|
+
*/
|
|
76
|
+
self.addEventListener('install', (event) => {
|
|
77
|
+
event.waitUntil(
|
|
78
|
+
(async () => {
|
|
79
|
+
const cache = await caches.open(CACHE_NAME);
|
|
80
|
+
|
|
81
|
+
// Cache essential assets
|
|
82
|
+
const essentialAssets = ['/', MANIFEST_URL, '/.aeon/runtime.js'];
|
|
83
|
+
|
|
84
|
+
await cache.addAll(essentialAssets.filter(Boolean));
|
|
85
|
+
|
|
86
|
+
// Skip waiting to activate immediately
|
|
87
|
+
await self.skipWaiting();
|
|
88
|
+
})(),
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Activate event - Clean up old caches and claim clients
|
|
94
|
+
*/
|
|
95
|
+
self.addEventListener('activate', (event) => {
|
|
96
|
+
event.waitUntil(
|
|
97
|
+
(async () => {
|
|
98
|
+
// Clean up old caches
|
|
99
|
+
const cacheNames = await caches.keys();
|
|
100
|
+
await Promise.all(
|
|
101
|
+
cacheNames
|
|
102
|
+
.filter((name) => name !== CACHE_NAME)
|
|
103
|
+
.map((name) => caches.delete(name)),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Claim all clients immediately
|
|
107
|
+
await self.clients.claim();
|
|
108
|
+
|
|
109
|
+
// Start total preload in background
|
|
110
|
+
startTotalPreload();
|
|
111
|
+
})(),
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Push event - Handle incoming push notifications
|
|
117
|
+
*/
|
|
118
|
+
self.addEventListener('push', (event) => {
|
|
119
|
+
handlePush(event, pushConfig);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Notification click event - Handle notification interactions
|
|
124
|
+
*/
|
|
125
|
+
self.addEventListener('notificationclick', (event) => {
|
|
126
|
+
handleNotificationClick(event, pushConfig);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Notification close event - Clean up on notification dismiss
|
|
131
|
+
*/
|
|
132
|
+
self.addEventListener('notificationclose', (event) => {
|
|
133
|
+
handleNotificationClose(event);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Sync event - Handle background sync for offline queue
|
|
138
|
+
*/
|
|
139
|
+
self.addEventListener('sync', (event: Event) => {
|
|
140
|
+
const syncEvent = event as ExtendableEvent & { tag: string };
|
|
141
|
+
handleSync(syncEvent, syncEvent.tag);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Fetch event - Serve from cache, fallback to network
|
|
146
|
+
*/
|
|
147
|
+
self.addEventListener('fetch', (event) => {
|
|
148
|
+
const url = new URL(event.request.url);
|
|
149
|
+
|
|
150
|
+
// Skip non-GET requests
|
|
151
|
+
if (event.request.method !== 'GET') {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Skip WebSocket and other special protocols
|
|
156
|
+
if (!url.protocol.startsWith('http')) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle session requests (highest priority for cached)
|
|
161
|
+
if (url.pathname.startsWith(SESSIONS_PREFIX)) {
|
|
162
|
+
event.respondWith(handleSessionRequest(event.request));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Handle page navigation requests
|
|
167
|
+
if (event.request.mode === 'navigate') {
|
|
168
|
+
event.respondWith(handleNavigationRequest(event.request));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Standard cache-first for other assets
|
|
173
|
+
event.respondWith(cacheFirst(event.request));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Handle session data requests - cache first, network fallback
|
|
178
|
+
*/
|
|
179
|
+
async function handleSessionRequest(request: Request): Promise<Response> {
|
|
180
|
+
const cache = await caches.open(CACHE_NAME);
|
|
181
|
+
const cached = await cache.match(request);
|
|
182
|
+
|
|
183
|
+
if (cached) {
|
|
184
|
+
// Return cached immediately, revalidate in background
|
|
185
|
+
revalidateInBackground(request, cache);
|
|
186
|
+
return cached;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Fetch and cache
|
|
190
|
+
const response = await fetch(request);
|
|
191
|
+
if (response.ok) {
|
|
192
|
+
cache.put(request, response.clone());
|
|
193
|
+
}
|
|
194
|
+
return response;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Handle navigation requests - try to serve pre-rendered page
|
|
199
|
+
*/
|
|
200
|
+
async function handleNavigationRequest(request: Request): Promise<Response> {
|
|
201
|
+
const cache = await caches.open(CACHE_NAME);
|
|
202
|
+
const url = new URL(request.url);
|
|
203
|
+
|
|
204
|
+
// Try to match the route to a cached session
|
|
205
|
+
const manifest = await getManifest(cache);
|
|
206
|
+
if (manifest) {
|
|
207
|
+
const route = matchRoute(url.pathname, manifest.routes);
|
|
208
|
+
if (route) {
|
|
209
|
+
const sessionUrl = `${SESSIONS_PREFIX}${route.sessionId}.json`;
|
|
210
|
+
const sessionResponse = await cache.match(sessionUrl);
|
|
211
|
+
|
|
212
|
+
if (sessionResponse) {
|
|
213
|
+
// We have the session - could render here or let client handle
|
|
214
|
+
// For now, pass through to let the client render with cached data
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Standard navigation handling
|
|
220
|
+
const cached = await cache.match(request);
|
|
221
|
+
if (cached) {
|
|
222
|
+
return cached;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return fetch(request);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Cache-first strategy
|
|
230
|
+
*/
|
|
231
|
+
async function cacheFirst(request: Request): Promise<Response> {
|
|
232
|
+
const cache = await caches.open(CACHE_NAME);
|
|
233
|
+
const cached = await cache.match(request);
|
|
234
|
+
|
|
235
|
+
if (cached) {
|
|
236
|
+
return cached;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const response = await fetch(request);
|
|
240
|
+
if (response.ok) {
|
|
241
|
+
cache.put(request, response.clone());
|
|
242
|
+
}
|
|
243
|
+
return response;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Revalidate in background (stale-while-revalidate)
|
|
248
|
+
*/
|
|
249
|
+
async function revalidateInBackground(
|
|
250
|
+
request: Request,
|
|
251
|
+
cache: Cache,
|
|
252
|
+
): Promise<void> {
|
|
253
|
+
try {
|
|
254
|
+
const response = await fetch(request);
|
|
255
|
+
if (response.ok) {
|
|
256
|
+
await cache.put(request, response);
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
// Ignore network errors during revalidation
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get cached manifest
|
|
265
|
+
*/
|
|
266
|
+
async function getManifest(cache: Cache): Promise<RouteManifest | null> {
|
|
267
|
+
try {
|
|
268
|
+
const response = await cache.match(MANIFEST_URL);
|
|
269
|
+
if (response) {
|
|
270
|
+
return response.json();
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// Ignore errors
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Match URL to route
|
|
280
|
+
*/
|
|
281
|
+
function matchRoute(
|
|
282
|
+
pathname: string,
|
|
283
|
+
routes: RouteManifest['routes'],
|
|
284
|
+
): RouteManifest['routes'][0] | null {
|
|
285
|
+
for (const route of routes) {
|
|
286
|
+
if (matchPattern(pathname, route.pattern)) {
|
|
287
|
+
return route;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Match URL pattern (supports dynamic segments)
|
|
295
|
+
*/
|
|
296
|
+
function matchPattern(pathname: string, pattern: string): boolean {
|
|
297
|
+
// Convert pattern to regex
|
|
298
|
+
const regexPattern = pattern
|
|
299
|
+
.replace(/\[\[\.\.\.(\w+)\]\]/g, '(?:.*)?') // Optional catch-all
|
|
300
|
+
.replace(/\[\.\.\.(\w+)\]/g, '(.+)') // Required catch-all
|
|
301
|
+
.replace(/\[(\w+)\]/g, '([^/]+)'); // Dynamic segment
|
|
302
|
+
|
|
303
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
304
|
+
return regex.test(pathname);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Start total preload of all sessions
|
|
309
|
+
*/
|
|
310
|
+
async function startTotalPreload(): Promise<void> {
|
|
311
|
+
const cache = await caches.open(CACHE_NAME);
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
// Fetch fresh manifest
|
|
315
|
+
const manifestResponse = await fetch(MANIFEST_URL);
|
|
316
|
+
if (!manifestResponse.ok) {
|
|
317
|
+
console.warn('[aeon-sw] Could not fetch manifest for total preload');
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const manifest: RouteManifest = await manifestResponse.json();
|
|
322
|
+
await cache.put(MANIFEST_URL, new Response(JSON.stringify(manifest)));
|
|
323
|
+
|
|
324
|
+
const total = manifest.routes.length;
|
|
325
|
+
let loaded = 0;
|
|
326
|
+
const cachedRoutes: string[] = [];
|
|
327
|
+
|
|
328
|
+
// Batch preload sessions
|
|
329
|
+
const batchSize = 5;
|
|
330
|
+
for (let i = 0; i < manifest.routes.length; i += batchSize) {
|
|
331
|
+
const batch = manifest.routes.slice(i, i + batchSize);
|
|
332
|
+
|
|
333
|
+
await Promise.all(
|
|
334
|
+
batch.map(async (route) => {
|
|
335
|
+
const sessionUrl = `${SESSIONS_PREFIX}${route.sessionId}.json`;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
// Check if already cached
|
|
339
|
+
const existing = await cache.match(sessionUrl);
|
|
340
|
+
if (!existing) {
|
|
341
|
+
const response = await fetch(sessionUrl);
|
|
342
|
+
if (response.ok) {
|
|
343
|
+
await cache.put(sessionUrl, response);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
cachedRoutes.push(route.pattern);
|
|
347
|
+
} catch {
|
|
348
|
+
// Ignore individual failures
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
loaded++;
|
|
352
|
+
|
|
353
|
+
// Broadcast progress to clients
|
|
354
|
+
broadcastProgress({
|
|
355
|
+
type: 'PRELOAD_PROGRESS',
|
|
356
|
+
loaded,
|
|
357
|
+
total,
|
|
358
|
+
percentage: Math.round((loaded / total) * 100),
|
|
359
|
+
});
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Small delay to keep main thread responsive
|
|
364
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Broadcast completion
|
|
368
|
+
broadcastProgress({
|
|
369
|
+
type: 'PRELOAD_COMPLETE',
|
|
370
|
+
loaded: total,
|
|
371
|
+
total,
|
|
372
|
+
percentage: 100,
|
|
373
|
+
cachedRoutes,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
console.log(`[aeon-sw] Total preload complete: ${total} sessions cached`);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error('[aeon-sw] Error during total preload:', error);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Broadcast message to all clients
|
|
384
|
+
*/
|
|
385
|
+
async function broadcastProgress(message: CacheMessage): Promise<void> {
|
|
386
|
+
const clients = await self.clients.matchAll({ type: 'window' });
|
|
387
|
+
clients.forEach((client) => {
|
|
388
|
+
client.postMessage(message);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Handle messages from clients
|
|
394
|
+
*/
|
|
395
|
+
self.addEventListener('message', (event) => {
|
|
396
|
+
const data = event.data;
|
|
397
|
+
|
|
398
|
+
switch (data.type) {
|
|
399
|
+
case 'GET_CACHE_STATUS':
|
|
400
|
+
handleGetCacheStatus(event);
|
|
401
|
+
break;
|
|
402
|
+
|
|
403
|
+
case 'TRIGGER_PRELOAD':
|
|
404
|
+
startTotalPreload();
|
|
405
|
+
break;
|
|
406
|
+
|
|
407
|
+
case 'PREFETCH_ROUTE':
|
|
408
|
+
handlePrefetchRoute(data.route);
|
|
409
|
+
break;
|
|
410
|
+
|
|
411
|
+
case 'CLEAR_CACHE':
|
|
412
|
+
handleClearCache();
|
|
413
|
+
break;
|
|
414
|
+
|
|
415
|
+
case 'GET_QUEUE_STATUS':
|
|
416
|
+
handleGetQueueStatus(event);
|
|
417
|
+
break;
|
|
418
|
+
|
|
419
|
+
case 'SYNC_NOW':
|
|
420
|
+
handleSyncNow();
|
|
421
|
+
break;
|
|
422
|
+
|
|
423
|
+
case 'GET_NETWORK_STATE':
|
|
424
|
+
handleGetNetworkState(event);
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Handle cache status request
|
|
431
|
+
*/
|
|
432
|
+
async function handleGetCacheStatus(
|
|
433
|
+
event: ExtendableMessageEvent,
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
const cache = await caches.open(CACHE_NAME);
|
|
436
|
+
const manifest = await getManifest(cache);
|
|
437
|
+
|
|
438
|
+
if (!manifest) {
|
|
439
|
+
event.ports[0]?.postMessage({ cached: 0, total: 0, routes: [] });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const cachedRoutes: string[] = [];
|
|
444
|
+
for (const route of manifest.routes) {
|
|
445
|
+
const sessionUrl = `${SESSIONS_PREFIX}${route.sessionId}.json`;
|
|
446
|
+
const cached = await cache.match(sessionUrl);
|
|
447
|
+
if (cached) {
|
|
448
|
+
cachedRoutes.push(route.pattern);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
event.ports[0]?.postMessage({
|
|
453
|
+
cached: cachedRoutes.length,
|
|
454
|
+
total: manifest.routes.length,
|
|
455
|
+
routes: cachedRoutes,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Handle prefetch route request
|
|
461
|
+
*/
|
|
462
|
+
async function handlePrefetchRoute(route: string): Promise<void> {
|
|
463
|
+
const cache = await caches.open(CACHE_NAME);
|
|
464
|
+
const manifest = await getManifest(cache);
|
|
465
|
+
|
|
466
|
+
if (!manifest) return;
|
|
467
|
+
|
|
468
|
+
const routeInfo = manifest.routes.find((r) => r.pattern === route);
|
|
469
|
+
if (!routeInfo) return;
|
|
470
|
+
|
|
471
|
+
const sessionUrl = `${SESSIONS_PREFIX}${routeInfo.sessionId}.json`;
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
const existing = await cache.match(sessionUrl);
|
|
475
|
+
if (!existing) {
|
|
476
|
+
const response = await fetch(sessionUrl);
|
|
477
|
+
if (response.ok) {
|
|
478
|
+
await cache.put(sessionUrl, response);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
482
|
+
// Ignore errors
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Handle clear cache request
|
|
488
|
+
*/
|
|
489
|
+
async function handleClearCache(): Promise<void> {
|
|
490
|
+
await caches.delete(CACHE_NAME);
|
|
491
|
+
console.log('[aeon-sw] Cache cleared');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Handle queue status request
|
|
496
|
+
* Returns stats about the offline operation queue
|
|
497
|
+
*/
|
|
498
|
+
async function handleGetQueueStatus(
|
|
499
|
+
event: ExtendableMessageEvent,
|
|
500
|
+
): Promise<void> {
|
|
501
|
+
// Queue stats would be stored in IndexedDB by the encrypted queue
|
|
502
|
+
// For now, we broadcast a message to get stats from the main thread
|
|
503
|
+
const clients = await self.clients.matchAll({ type: 'window' });
|
|
504
|
+
|
|
505
|
+
if (clients.length > 0) {
|
|
506
|
+
// Ask the first client for queue stats
|
|
507
|
+
clients[0].postMessage({ type: 'GET_QUEUE_STATS_REQUEST' });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Respond with placeholder - actual stats come from main thread
|
|
511
|
+
event.ports[0]?.postMessage({
|
|
512
|
+
pending: 0,
|
|
513
|
+
syncing: 0,
|
|
514
|
+
failed: 0,
|
|
515
|
+
totalBytes: 0,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Handle sync now request
|
|
521
|
+
* Triggers immediate sync of offline queue
|
|
522
|
+
*/
|
|
523
|
+
async function handleSyncNow(): Promise<void> {
|
|
524
|
+
// Broadcast to all clients to trigger sync
|
|
525
|
+
const clients = await self.clients.matchAll({ type: 'window' });
|
|
526
|
+
clients.forEach((client) => {
|
|
527
|
+
client.postMessage({ type: 'SYNC_OFFLINE_QUEUE', timestamp: Date.now() });
|
|
528
|
+
});
|
|
529
|
+
console.log('[aeon-sw] Sync triggered');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Handle network state request
|
|
534
|
+
*/
|
|
535
|
+
async function handleGetNetworkState(
|
|
536
|
+
event: ExtendableMessageEvent,
|
|
537
|
+
): Promise<void> {
|
|
538
|
+
// Check if we can reach the origin
|
|
539
|
+
let isOnline = true;
|
|
540
|
+
try {
|
|
541
|
+
const response = await fetch(MANIFEST_URL, { method: 'HEAD' });
|
|
542
|
+
isOnline = response.ok;
|
|
543
|
+
} catch {
|
|
544
|
+
isOnline = false;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
event.ports[0]?.postMessage({
|
|
548
|
+
state: isOnline ? 'online' : 'offline',
|
|
549
|
+
timestamp: Date.now(),
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export {};
|