@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,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useConflicts Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for managing conflicts in offline-first applications.
|
|
5
|
+
* Provides access to conflict list, resolution methods, and statistics.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export type ResolutionStrategy =
|
|
15
|
+
| 'local-wins'
|
|
16
|
+
| 'remote-wins'
|
|
17
|
+
| 'merge'
|
|
18
|
+
| 'last-modified'
|
|
19
|
+
| 'manual';
|
|
20
|
+
|
|
21
|
+
export interface Conflict {
|
|
22
|
+
id: string;
|
|
23
|
+
operationId: string;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
localData: Record<string, unknown>;
|
|
26
|
+
remoteData: Record<string, unknown>;
|
|
27
|
+
type: 'update_update' | 'delete_update' | 'update_delete' | 'concurrent';
|
|
28
|
+
severity: 'low' | 'medium' | 'high';
|
|
29
|
+
detectedAt: number;
|
|
30
|
+
resolution?: {
|
|
31
|
+
strategy: ResolutionStrategy;
|
|
32
|
+
resolvedData: Record<string, unknown>;
|
|
33
|
+
resolvedAt: number;
|
|
34
|
+
resolvedBy?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ConflictStats {
|
|
39
|
+
total: number;
|
|
40
|
+
unresolved: number;
|
|
41
|
+
highSeverity: number;
|
|
42
|
+
byType: Record<Conflict['type'], number>;
|
|
43
|
+
byStrategy: Record<ResolutionStrategy, number>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UseConflictsResult {
|
|
47
|
+
/** All conflicts */
|
|
48
|
+
conflicts: Conflict[];
|
|
49
|
+
/** Unresolved conflicts only */
|
|
50
|
+
unresolvedConflicts: Conflict[];
|
|
51
|
+
/** High severity conflicts */
|
|
52
|
+
highSeverityConflicts: Conflict[];
|
|
53
|
+
/** Conflict statistics */
|
|
54
|
+
stats: ConflictStats;
|
|
55
|
+
/** Resolve a conflict */
|
|
56
|
+
resolveConflict: (
|
|
57
|
+
conflictId: string,
|
|
58
|
+
strategy: ResolutionStrategy,
|
|
59
|
+
customData?: Record<string, unknown>,
|
|
60
|
+
) => Promise<void>;
|
|
61
|
+
/** Dismiss a conflict (remove without resolution) */
|
|
62
|
+
dismissConflict: (conflictId: string) => void;
|
|
63
|
+
/** Clear all resolved conflicts */
|
|
64
|
+
clearResolved: () => void;
|
|
65
|
+
/** Refresh conflicts from source */
|
|
66
|
+
refresh: () => void;
|
|
67
|
+
/** Whether conflicts are loading */
|
|
68
|
+
isLoading: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Mock conflict resolver (would connect to actual ConflictResolver in production)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
const conflictStore: Map<string, Conflict> = new Map();
|
|
76
|
+
const listeners: Set<() => void> = new Set();
|
|
77
|
+
|
|
78
|
+
function notifyListeners() {
|
|
79
|
+
listeners.forEach((listener) => listener());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Hook Implementation
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Hook to manage conflicts in offline-first applications
|
|
88
|
+
*/
|
|
89
|
+
export function useConflicts(sessionId?: string): UseConflictsResult {
|
|
90
|
+
const [conflicts, setConflicts] = useState<Conflict[]>([]);
|
|
91
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
92
|
+
|
|
93
|
+
const loadConflicts = useCallback(() => {
|
|
94
|
+
const allConflicts = Array.from(conflictStore.values());
|
|
95
|
+
const filtered = sessionId
|
|
96
|
+
? allConflicts.filter((c) => c.sessionId === sessionId)
|
|
97
|
+
: allConflicts;
|
|
98
|
+
setConflicts(filtered);
|
|
99
|
+
}, [sessionId]);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
loadConflicts();
|
|
103
|
+
|
|
104
|
+
const listener = () => loadConflicts();
|
|
105
|
+
listeners.add(listener);
|
|
106
|
+
|
|
107
|
+
return () => {
|
|
108
|
+
listeners.delete(listener);
|
|
109
|
+
};
|
|
110
|
+
}, [loadConflicts]);
|
|
111
|
+
|
|
112
|
+
const unresolvedConflicts = useMemo(
|
|
113
|
+
() => conflicts.filter((c) => !c.resolution),
|
|
114
|
+
[conflicts],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const highSeverityConflicts = useMemo(
|
|
118
|
+
() => conflicts.filter((c) => !c.resolution && c.severity === 'high'),
|
|
119
|
+
[conflicts],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const stats = useMemo<ConflictStats>(() => {
|
|
123
|
+
const byType: Record<Conflict['type'], number> = {
|
|
124
|
+
update_update: 0,
|
|
125
|
+
delete_update: 0,
|
|
126
|
+
update_delete: 0,
|
|
127
|
+
concurrent: 0,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const byStrategy: Record<ResolutionStrategy, number> = {
|
|
131
|
+
'local-wins': 0,
|
|
132
|
+
'remote-wins': 0,
|
|
133
|
+
merge: 0,
|
|
134
|
+
'last-modified': 0,
|
|
135
|
+
manual: 0,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
let unresolved = 0;
|
|
139
|
+
let highSeverity = 0;
|
|
140
|
+
|
|
141
|
+
for (const conflict of conflicts) {
|
|
142
|
+
byType[conflict.type]++;
|
|
143
|
+
|
|
144
|
+
if (!conflict.resolution) {
|
|
145
|
+
unresolved++;
|
|
146
|
+
if (conflict.severity === 'high') {
|
|
147
|
+
highSeverity++;
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
byStrategy[conflict.resolution.strategy]++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
total: conflicts.length,
|
|
156
|
+
unresolved,
|
|
157
|
+
highSeverity,
|
|
158
|
+
byType,
|
|
159
|
+
byStrategy,
|
|
160
|
+
};
|
|
161
|
+
}, [conflicts]);
|
|
162
|
+
|
|
163
|
+
const resolveConflict = useCallback(
|
|
164
|
+
async (
|
|
165
|
+
conflictId: string,
|
|
166
|
+
strategy: ResolutionStrategy,
|
|
167
|
+
customData?: Record<string, unknown>,
|
|
168
|
+
) => {
|
|
169
|
+
setIsLoading(true);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const conflict = conflictStore.get(conflictId);
|
|
173
|
+
if (!conflict) {
|
|
174
|
+
throw new Error(`Conflict ${conflictId} not found`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let resolvedData: Record<string, unknown>;
|
|
178
|
+
|
|
179
|
+
switch (strategy) {
|
|
180
|
+
case 'local-wins':
|
|
181
|
+
resolvedData = conflict.localData;
|
|
182
|
+
break;
|
|
183
|
+
case 'remote-wins':
|
|
184
|
+
resolvedData = conflict.remoteData;
|
|
185
|
+
break;
|
|
186
|
+
case 'merge':
|
|
187
|
+
// Simple merge - combine both, local wins for conflicts
|
|
188
|
+
resolvedData = { ...conflict.remoteData, ...conflict.localData };
|
|
189
|
+
break;
|
|
190
|
+
case 'last-modified':
|
|
191
|
+
// Default to local
|
|
192
|
+
resolvedData = conflict.localData;
|
|
193
|
+
break;
|
|
194
|
+
case 'manual':
|
|
195
|
+
if (!customData) {
|
|
196
|
+
throw new Error('Manual resolution requires customData');
|
|
197
|
+
}
|
|
198
|
+
resolvedData = customData;
|
|
199
|
+
break;
|
|
200
|
+
default:
|
|
201
|
+
resolvedData = conflict.localData;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
conflict.resolution = {
|
|
205
|
+
strategy,
|
|
206
|
+
resolvedData,
|
|
207
|
+
resolvedAt: Date.now(),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
conflictStore.set(conflictId, conflict);
|
|
211
|
+
notifyListeners();
|
|
212
|
+
} finally {
|
|
213
|
+
setIsLoading(false);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
[],
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const dismissConflict = useCallback((conflictId: string) => {
|
|
220
|
+
conflictStore.delete(conflictId);
|
|
221
|
+
notifyListeners();
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
224
|
+
const clearResolved = useCallback(() => {
|
|
225
|
+
for (const [id, conflict] of conflictStore) {
|
|
226
|
+
if (conflict.resolution) {
|
|
227
|
+
conflictStore.delete(id);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
notifyListeners();
|
|
231
|
+
}, []);
|
|
232
|
+
|
|
233
|
+
const refresh = useCallback(() => {
|
|
234
|
+
loadConflicts();
|
|
235
|
+
}, [loadConflicts]);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
conflicts,
|
|
239
|
+
unresolvedConflicts,
|
|
240
|
+
highSeverityConflicts,
|
|
241
|
+
stats,
|
|
242
|
+
resolveConflict,
|
|
243
|
+
dismissConflict,
|
|
244
|
+
clearResolved,
|
|
245
|
+
refresh,
|
|
246
|
+
isLoading,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ============================================================================
|
|
251
|
+
// Helper functions for external use
|
|
252
|
+
// ============================================================================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Add a conflict to the store (for testing or external integration)
|
|
256
|
+
*/
|
|
257
|
+
export function addConflict(conflict: Conflict): void {
|
|
258
|
+
conflictStore.set(conflict.id, conflict);
|
|
259
|
+
notifyListeners();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get all conflicts from the store
|
|
264
|
+
*/
|
|
265
|
+
export function getAllConflicts(): Conflict[] {
|
|
266
|
+
return Array.from(conflictStore.values());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clear all conflicts
|
|
271
|
+
*/
|
|
272
|
+
export function clearAllConflicts(): void {
|
|
273
|
+
conflictStore.clear();
|
|
274
|
+
notifyListeners();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default useConflicts;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useNetworkState Hook
|
|
3
|
+
*
|
|
4
|
+
* React hook for monitoring network state and bandwidth.
|
|
5
|
+
* Provides real-time updates on connection quality and effective type.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export type NetworkState = 'online' | 'offline' | 'poor' | 'unknown';
|
|
15
|
+
|
|
16
|
+
export interface BandwidthProfile {
|
|
17
|
+
/** Estimated bandwidth in Kbps */
|
|
18
|
+
speedKbps: number;
|
|
19
|
+
/** Estimated latency in ms */
|
|
20
|
+
latencyMs: number;
|
|
21
|
+
/** Reliability score (0-1) */
|
|
22
|
+
reliability: number;
|
|
23
|
+
/** Effective connection type */
|
|
24
|
+
effectiveType: '2g' | '3g' | '4g' | 'slow-2g' | 'unknown';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface NetworkStateResult {
|
|
28
|
+
/** Current network state */
|
|
29
|
+
state: NetworkState;
|
|
30
|
+
/** Whether currently online */
|
|
31
|
+
isOnline: boolean;
|
|
32
|
+
/** Whether on poor connection */
|
|
33
|
+
isPoor: boolean;
|
|
34
|
+
/** Bandwidth profile */
|
|
35
|
+
bandwidth: BandwidthProfile;
|
|
36
|
+
/** Time since last state change */
|
|
37
|
+
timeSinceChange: number;
|
|
38
|
+
/** Force refresh network state */
|
|
39
|
+
refresh: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Network Information API types
|
|
43
|
+
interface NetworkInformation {
|
|
44
|
+
effectiveType?: '2g' | '3g' | '4g' | 'slow-2g';
|
|
45
|
+
downlink?: number;
|
|
46
|
+
rtt?: number;
|
|
47
|
+
saveData?: boolean;
|
|
48
|
+
addEventListener?: (event: string, callback: () => void) => void;
|
|
49
|
+
removeEventListener?: (event: string, callback: () => void) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface NavigatorWithConnection extends Navigator {
|
|
53
|
+
connection?: NetworkInformation;
|
|
54
|
+
mozConnection?: NetworkInformation;
|
|
55
|
+
webkitConnection?: NetworkInformation;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Hook Implementation
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Hook to monitor network state and bandwidth
|
|
64
|
+
*/
|
|
65
|
+
export function useNetworkState(): NetworkStateResult {
|
|
66
|
+
const [state, setState] = useState<NetworkState>('unknown');
|
|
67
|
+
const [lastChange, setLastChange] = useState(Date.now());
|
|
68
|
+
const [bandwidth, setBandwidth] = useState<BandwidthProfile>({
|
|
69
|
+
speedKbps: 1024,
|
|
70
|
+
latencyMs: 50,
|
|
71
|
+
reliability: 1,
|
|
72
|
+
effectiveType: 'unknown',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const getConnection = useCallback((): NetworkInformation | undefined => {
|
|
76
|
+
if (typeof navigator === 'undefined') return undefined;
|
|
77
|
+
const nav = navigator as NavigatorWithConnection;
|
|
78
|
+
return nav.connection || nav.mozConnection || nav.webkitConnection;
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const updateBandwidth = useCallback(() => {
|
|
82
|
+
const conn = getConnection();
|
|
83
|
+
if (!conn) return;
|
|
84
|
+
|
|
85
|
+
const effectiveType = conn.effectiveType || 'unknown';
|
|
86
|
+
let speedKbps = 1024;
|
|
87
|
+
let latencyMs = 50;
|
|
88
|
+
let reliability = 1;
|
|
89
|
+
|
|
90
|
+
switch (effectiveType) {
|
|
91
|
+
case 'slow-2g':
|
|
92
|
+
speedKbps = 50;
|
|
93
|
+
latencyMs = 2000;
|
|
94
|
+
reliability = 0.5;
|
|
95
|
+
break;
|
|
96
|
+
case '2g':
|
|
97
|
+
speedKbps = 150;
|
|
98
|
+
latencyMs = 1000;
|
|
99
|
+
reliability = 0.7;
|
|
100
|
+
break;
|
|
101
|
+
case '3g':
|
|
102
|
+
speedKbps = 750;
|
|
103
|
+
latencyMs = 400;
|
|
104
|
+
reliability = 0.85;
|
|
105
|
+
break;
|
|
106
|
+
case '4g':
|
|
107
|
+
speedKbps = 5000;
|
|
108
|
+
latencyMs = 50;
|
|
109
|
+
reliability = 0.95;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Use actual values if available
|
|
114
|
+
if (conn.downlink) {
|
|
115
|
+
speedKbps = conn.downlink * 1024;
|
|
116
|
+
}
|
|
117
|
+
if (conn.rtt) {
|
|
118
|
+
latencyMs = conn.rtt;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setBandwidth({
|
|
122
|
+
speedKbps,
|
|
123
|
+
latencyMs,
|
|
124
|
+
reliability,
|
|
125
|
+
effectiveType,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Update state based on connection quality
|
|
129
|
+
if (effectiveType === 'slow-2g' || effectiveType === '2g') {
|
|
130
|
+
setState((prev) => {
|
|
131
|
+
if (prev !== 'poor') {
|
|
132
|
+
setLastChange(Date.now());
|
|
133
|
+
}
|
|
134
|
+
return 'poor';
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}, [getConnection]);
|
|
138
|
+
|
|
139
|
+
const updateOnlineState = useCallback(() => {
|
|
140
|
+
if (typeof navigator === 'undefined') return;
|
|
141
|
+
|
|
142
|
+
const isOnline = navigator.onLine;
|
|
143
|
+
setState((prev) => {
|
|
144
|
+
const newState = isOnline ? 'online' : 'offline';
|
|
145
|
+
if (prev !== newState) {
|
|
146
|
+
setLastChange(Date.now());
|
|
147
|
+
}
|
|
148
|
+
return newState;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (isOnline) {
|
|
152
|
+
updateBandwidth();
|
|
153
|
+
}
|
|
154
|
+
}, [updateBandwidth]);
|
|
155
|
+
|
|
156
|
+
const refresh = useCallback(() => {
|
|
157
|
+
updateOnlineState();
|
|
158
|
+
}, [updateOnlineState]);
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
// Initial state
|
|
162
|
+
updateOnlineState();
|
|
163
|
+
|
|
164
|
+
// Listen for online/offline events
|
|
165
|
+
const handleOnline = () => {
|
|
166
|
+
setState('online');
|
|
167
|
+
setLastChange(Date.now());
|
|
168
|
+
updateBandwidth();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleOffline = () => {
|
|
172
|
+
setState('offline');
|
|
173
|
+
setLastChange(Date.now());
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (typeof window !== 'undefined') {
|
|
177
|
+
window.addEventListener('online', handleOnline);
|
|
178
|
+
window.addEventListener('offline', handleOffline);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Listen for connection changes
|
|
182
|
+
const conn = getConnection();
|
|
183
|
+
if (conn?.addEventListener) {
|
|
184
|
+
conn.addEventListener('change', updateBandwidth);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return () => {
|
|
188
|
+
if (typeof window !== 'undefined') {
|
|
189
|
+
window.removeEventListener('online', handleOnline);
|
|
190
|
+
window.removeEventListener('offline', handleOffline);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (conn?.removeEventListener) {
|
|
194
|
+
conn.removeEventListener('change', updateBandwidth);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}, [getConnection, updateBandwidth, updateOnlineState]);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
state,
|
|
201
|
+
isOnline: state === 'online' || state === 'poor',
|
|
202
|
+
isPoor: state === 'poor',
|
|
203
|
+
bandwidth,
|
|
204
|
+
timeSinceChange: Date.now() - lastChange,
|
|
205
|
+
refresh,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default useNetworkState;
|