@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. 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 {};