@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,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic Router Adapter
|
|
3
|
+
*
|
|
4
|
+
* Zero-latency personalized routing using pure heuristics.
|
|
5
|
+
* No external API calls - all decisions made locally via WASM-compatible logic.
|
|
6
|
+
*
|
|
7
|
+
* Signals used:
|
|
8
|
+
* - User tier → feature gating
|
|
9
|
+
* - Viewport → responsive layout selection, density
|
|
10
|
+
* - Custom signals → theme/accent derivation (configurable)
|
|
11
|
+
* - Navigation history → component ordering, speculation
|
|
12
|
+
* - Time of day → theme suggestion
|
|
13
|
+
* - Connection speed → prefetch depth
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
ComponentNode,
|
|
18
|
+
ComponentTree,
|
|
19
|
+
EmotionState,
|
|
20
|
+
LayoutDensity,
|
|
21
|
+
RouteDecision,
|
|
22
|
+
RouterAdapter,
|
|
23
|
+
SkeletonHints,
|
|
24
|
+
ThemeMode,
|
|
25
|
+
UserContext,
|
|
26
|
+
UserTier,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Configuration Types
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Feature flags configuration by tier
|
|
35
|
+
*/
|
|
36
|
+
export type TierFeatures = Record<UserTier, Record<string, boolean>>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Custom signal processor for deriving values from user context
|
|
40
|
+
*/
|
|
41
|
+
export interface SignalProcessor {
|
|
42
|
+
/** Derive accent color from context */
|
|
43
|
+
deriveAccent?: (context: UserContext) => string;
|
|
44
|
+
|
|
45
|
+
/** Derive theme from context */
|
|
46
|
+
deriveTheme?: (context: UserContext) => ThemeMode;
|
|
47
|
+
|
|
48
|
+
/** Custom component relevance scoring */
|
|
49
|
+
scoreRelevance?: (node: ComponentNode, context: UserContext) => number;
|
|
50
|
+
|
|
51
|
+
/** Custom navigation prediction */
|
|
52
|
+
predictNavigation?: (currentPath: string, context: UserContext) => string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Heuristic adapter configuration
|
|
57
|
+
*/
|
|
58
|
+
export interface HeuristicAdapterConfig {
|
|
59
|
+
/** Feature flags by tier (optional - defaults to all features enabled) */
|
|
60
|
+
tierFeatures?: TierFeatures;
|
|
61
|
+
|
|
62
|
+
/** Default accent color when no signal processor provided */
|
|
63
|
+
defaultAccent?: string;
|
|
64
|
+
|
|
65
|
+
/** Custom signal processing */
|
|
66
|
+
signals?: SignalProcessor;
|
|
67
|
+
|
|
68
|
+
/** Default paths to suggest when no history available */
|
|
69
|
+
defaultPaths?: string[];
|
|
70
|
+
|
|
71
|
+
/** Maximum number of paths to speculate */
|
|
72
|
+
maxSpeculationPaths?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Default Configuration
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
const DEFAULT_CONFIG: Required<HeuristicAdapterConfig> = {
|
|
80
|
+
tierFeatures: {
|
|
81
|
+
free: {},
|
|
82
|
+
starter: {},
|
|
83
|
+
pro: {},
|
|
84
|
+
enterprise: {},
|
|
85
|
+
admin: {}, // Admins get all features
|
|
86
|
+
},
|
|
87
|
+
defaultAccent: '#336699', // Steel blue - neutral default
|
|
88
|
+
signals: {},
|
|
89
|
+
defaultPaths: ['/'],
|
|
90
|
+
maxSpeculationPaths: 5,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Theme Derivation
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Default theme derivation based on time
|
|
99
|
+
*/
|
|
100
|
+
function defaultDeriveTheme(context: UserContext): ThemeMode {
|
|
101
|
+
// Explicit preference takes priority
|
|
102
|
+
if (context.preferences.theme) {
|
|
103
|
+
return context.preferences.theme as ThemeMode;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Time-based suggestion
|
|
107
|
+
const hour = context.localHour;
|
|
108
|
+
const isNight = hour >= 20 || hour < 6;
|
|
109
|
+
const isEvening = hour >= 18 && hour < 20;
|
|
110
|
+
|
|
111
|
+
if (isNight) {
|
|
112
|
+
return 'dark';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (isEvening) {
|
|
116
|
+
return 'auto';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return 'light';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Determine layout density based on viewport and preferences
|
|
124
|
+
*/
|
|
125
|
+
function determineDensity(context: UserContext): LayoutDensity {
|
|
126
|
+
// Explicit preference
|
|
127
|
+
if (context.preferences.density) {
|
|
128
|
+
return context.preferences.density as LayoutDensity;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Viewport-based
|
|
132
|
+
const { width, height } = context.viewport;
|
|
133
|
+
|
|
134
|
+
// Mobile
|
|
135
|
+
if (width < 768) {
|
|
136
|
+
return 'compact';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Large desktop with plenty of space
|
|
140
|
+
if (width >= 1440 && height >= 900) {
|
|
141
|
+
return 'comfortable';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Default
|
|
145
|
+
return 'normal';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Navigation Prediction (Markov Chain)
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
interface TransitionMatrix {
|
|
153
|
+
[from: string]: { [to: string]: number };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build transition matrix from navigation history
|
|
158
|
+
*/
|
|
159
|
+
function buildTransitionMatrix(history: string[]): TransitionMatrix {
|
|
160
|
+
const matrix: TransitionMatrix = {};
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < history.length - 1; i++) {
|
|
163
|
+
const from = history[i];
|
|
164
|
+
const to = history[i + 1];
|
|
165
|
+
|
|
166
|
+
if (!matrix[from]) {
|
|
167
|
+
matrix[from] = {};
|
|
168
|
+
}
|
|
169
|
+
matrix[from][to] = (matrix[from][to] || 0) + 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Normalize to probabilities
|
|
173
|
+
for (const from of Object.keys(matrix)) {
|
|
174
|
+
const total = Object.values(matrix[from]).reduce((a, b) => a + b, 0);
|
|
175
|
+
for (const to of Object.keys(matrix[from])) {
|
|
176
|
+
matrix[from][to] /= total;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return matrix;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Predict next routes based on current path and history
|
|
185
|
+
*/
|
|
186
|
+
function defaultPredictNavigation(
|
|
187
|
+
currentPath: string,
|
|
188
|
+
context: UserContext,
|
|
189
|
+
defaultPaths: string[],
|
|
190
|
+
topN: number,
|
|
191
|
+
): string[] {
|
|
192
|
+
const history = context.recentPages;
|
|
193
|
+
|
|
194
|
+
// If we have enough history, use Markov chain
|
|
195
|
+
if (history.length >= 3) {
|
|
196
|
+
const matrix = buildTransitionMatrix(history);
|
|
197
|
+
const transitions = matrix[currentPath];
|
|
198
|
+
|
|
199
|
+
if (transitions) {
|
|
200
|
+
const sorted = Object.entries(transitions)
|
|
201
|
+
.sort(([, a], [, b]) => b - a)
|
|
202
|
+
.slice(0, topN)
|
|
203
|
+
.map(([path]) => path);
|
|
204
|
+
|
|
205
|
+
if (sorted.length > 0) {
|
|
206
|
+
return sorted;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Fallback to default paths (excluding current)
|
|
212
|
+
return defaultPaths.filter((p) => p !== currentPath).slice(0, topN);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// Component Relevance Scoring
|
|
217
|
+
// ============================================================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Default component relevance scoring
|
|
221
|
+
*/
|
|
222
|
+
function defaultScoreRelevance(
|
|
223
|
+
node: ComponentNode,
|
|
224
|
+
context: UserContext,
|
|
225
|
+
): number {
|
|
226
|
+
let score = 50; // Base score
|
|
227
|
+
|
|
228
|
+
// Tier gating
|
|
229
|
+
if (node.requiredTier) {
|
|
230
|
+
const tierOrder: UserTier[] = ['free', 'starter', 'pro', 'enterprise'];
|
|
231
|
+
const requiredIndex = tierOrder.indexOf(node.requiredTier);
|
|
232
|
+
const userIndex = tierOrder.indexOf(context.tier);
|
|
233
|
+
|
|
234
|
+
if (userIndex < requiredIndex) {
|
|
235
|
+
return 0; // User doesn't have access
|
|
236
|
+
}
|
|
237
|
+
score += 10;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Relevance signals
|
|
241
|
+
if (node.relevanceSignals) {
|
|
242
|
+
for (const signal of node.relevanceSignals) {
|
|
243
|
+
// Recent pages signal
|
|
244
|
+
if (signal.startsWith('recentPage:')) {
|
|
245
|
+
const page = signal.slice('recentPage:'.length);
|
|
246
|
+
if (context.recentPages.includes(page)) {
|
|
247
|
+
score += 20;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Time signal
|
|
252
|
+
if (signal.startsWith('timeOfDay:')) {
|
|
253
|
+
const timeRange = signal.slice('timeOfDay:'.length);
|
|
254
|
+
const hour = context.localHour;
|
|
255
|
+
|
|
256
|
+
if (timeRange === 'morning' && hour >= 5 && hour < 12) score += 15;
|
|
257
|
+
if (timeRange === 'afternoon' && hour >= 12 && hour < 17) score += 15;
|
|
258
|
+
if (timeRange === 'evening' && hour >= 17 && hour < 21) score += 15;
|
|
259
|
+
if (timeRange === 'night' && (hour >= 21 || hour < 5)) score += 15;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Preference signal
|
|
263
|
+
if (signal.startsWith('preference:')) {
|
|
264
|
+
const pref = signal.slice('preference:'.length);
|
|
265
|
+
if (context.preferences[pref]) {
|
|
266
|
+
score += 20;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Tier signal
|
|
271
|
+
if (signal.startsWith('tier:')) {
|
|
272
|
+
const requiredTier = signal.slice('tier:'.length) as UserTier;
|
|
273
|
+
const tierOrder: UserTier[] = ['free', 'starter', 'pro', 'enterprise'];
|
|
274
|
+
if (
|
|
275
|
+
tierOrder.indexOf(context.tier) >= tierOrder.indexOf(requiredTier)
|
|
276
|
+
) {
|
|
277
|
+
score += 15;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Default hidden penalty
|
|
284
|
+
if (node.defaultHidden) {
|
|
285
|
+
score -= 30;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return Math.max(0, Math.min(100, score));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Order components by relevance
|
|
293
|
+
*/
|
|
294
|
+
function orderComponentsByRelevance(
|
|
295
|
+
tree: ComponentTree,
|
|
296
|
+
context: UserContext,
|
|
297
|
+
scoreRelevance: (node: ComponentNode, context: UserContext) => number,
|
|
298
|
+
): string[] {
|
|
299
|
+
const scored: Array<{ id: string; score: number }> = [];
|
|
300
|
+
|
|
301
|
+
tree.nodes.forEach((node, id) => {
|
|
302
|
+
scored.push({
|
|
303
|
+
id,
|
|
304
|
+
score: scoreRelevance(node, context),
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return scored.sort((a, b) => b.score - a.score).map((s) => s.id);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Find components to hide based on tier and relevance
|
|
313
|
+
*/
|
|
314
|
+
function findHiddenComponents(
|
|
315
|
+
tree: ComponentTree,
|
|
316
|
+
context: UserContext,
|
|
317
|
+
scoreRelevance: (node: ComponentNode, context: UserContext) => number,
|
|
318
|
+
): string[] {
|
|
319
|
+
const hidden: string[] = [];
|
|
320
|
+
|
|
321
|
+
tree.nodes.forEach((node, id) => {
|
|
322
|
+
const score = scoreRelevance(node, context);
|
|
323
|
+
if (score === 0) {
|
|
324
|
+
hidden.push(id);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return hidden;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ============================================================================
|
|
332
|
+
// Skeleton Hints
|
|
333
|
+
// ============================================================================
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Compute skeleton hints for the route
|
|
337
|
+
*/
|
|
338
|
+
function computeSkeletonHints(
|
|
339
|
+
route: string,
|
|
340
|
+
context: UserContext,
|
|
341
|
+
tree: ComponentTree,
|
|
342
|
+
): SkeletonHints {
|
|
343
|
+
// Determine layout type from route - apps can override via custom signals
|
|
344
|
+
let layout: SkeletonHints['layout'] = 'custom';
|
|
345
|
+
|
|
346
|
+
// Simple path-based heuristics (can be overridden by app)
|
|
347
|
+
if (route === '/' || route.includes('dashboard')) {
|
|
348
|
+
layout = 'dashboard';
|
|
349
|
+
} else if (route.includes('chat') || route.includes('message')) {
|
|
350
|
+
layout = 'chat';
|
|
351
|
+
} else if (route.includes('setting') || route.includes('config')) {
|
|
352
|
+
layout = 'settings';
|
|
353
|
+
} else if (route.includes('tool')) {
|
|
354
|
+
layout = 'tools';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Estimate height based on viewport and content
|
|
358
|
+
const baseHeight = context.viewport.height;
|
|
359
|
+
const contentMultiplier = tree.nodes.size > 10 ? 1.5 : 1;
|
|
360
|
+
const estimatedHeight = Math.round(baseHeight * contentMultiplier);
|
|
361
|
+
|
|
362
|
+
// Compute section hints
|
|
363
|
+
const sections = tree.getChildren(tree.rootId).map((child, i) => ({
|
|
364
|
+
id: child.id,
|
|
365
|
+
height: Math.round(estimatedHeight / (tree.nodes.size || 1)),
|
|
366
|
+
priority: i + 1,
|
|
367
|
+
}));
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
layout,
|
|
371
|
+
estimatedHeight,
|
|
372
|
+
sections,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ============================================================================
|
|
377
|
+
// Prefetch Depth by Connection
|
|
378
|
+
// ============================================================================
|
|
379
|
+
|
|
380
|
+
function getPrefetchDepth(context: UserContext): {
|
|
381
|
+
prefetch: number;
|
|
382
|
+
prerender: number;
|
|
383
|
+
} {
|
|
384
|
+
switch (context.connection) {
|
|
385
|
+
case 'fast':
|
|
386
|
+
case '4g':
|
|
387
|
+
return { prefetch: 5, prerender: 1 };
|
|
388
|
+
case '3g':
|
|
389
|
+
return { prefetch: 3, prerender: 0 };
|
|
390
|
+
case '2g':
|
|
391
|
+
return { prefetch: 1, prerender: 0 };
|
|
392
|
+
case 'slow-2g':
|
|
393
|
+
return { prefetch: 0, prerender: 0 };
|
|
394
|
+
default:
|
|
395
|
+
return { prefetch: 3, prerender: 0 };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ============================================================================
|
|
400
|
+
// Heuristic Adapter Implementation
|
|
401
|
+
// ============================================================================
|
|
402
|
+
|
|
403
|
+
export class HeuristicAdapter implements RouterAdapter {
|
|
404
|
+
name = 'heuristic';
|
|
405
|
+
private config: Required<HeuristicAdapterConfig>;
|
|
406
|
+
|
|
407
|
+
constructor(config: HeuristicAdapterConfig = {}) {
|
|
408
|
+
this.config = {
|
|
409
|
+
...DEFAULT_CONFIG,
|
|
410
|
+
...config,
|
|
411
|
+
tierFeatures: config.tierFeatures ?? DEFAULT_CONFIG.tierFeatures,
|
|
412
|
+
signals: config.signals ?? DEFAULT_CONFIG.signals,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async route(
|
|
417
|
+
path: string,
|
|
418
|
+
context: UserContext,
|
|
419
|
+
tree: ComponentTree,
|
|
420
|
+
): Promise<RouteDecision> {
|
|
421
|
+
const startTime = Date.now();
|
|
422
|
+
|
|
423
|
+
// Generate session ID
|
|
424
|
+
const sessionId = this.generateSessionId(path, context);
|
|
425
|
+
|
|
426
|
+
// Compute feature flags from tier
|
|
427
|
+
const featureFlags = { ...this.config.tierFeatures[context.tier] };
|
|
428
|
+
|
|
429
|
+
// Compute theme - use custom processor or default
|
|
430
|
+
const theme = this.config.signals.deriveTheme
|
|
431
|
+
? this.config.signals.deriveTheme(context)
|
|
432
|
+
: defaultDeriveTheme(context);
|
|
433
|
+
|
|
434
|
+
// Compute accent - use custom processor or default
|
|
435
|
+
const accent = this.config.signals.deriveAccent
|
|
436
|
+
? this.config.signals.deriveAccent(context)
|
|
437
|
+
: this.config.defaultAccent;
|
|
438
|
+
|
|
439
|
+
// Compute density
|
|
440
|
+
const density = determineDensity(context);
|
|
441
|
+
|
|
442
|
+
// Relevance scoring - use custom or default
|
|
443
|
+
const scoreRelevance =
|
|
444
|
+
this.config.signals.scoreRelevance ?? defaultScoreRelevance;
|
|
445
|
+
|
|
446
|
+
// Order components by relevance
|
|
447
|
+
const componentOrder = orderComponentsByRelevance(
|
|
448
|
+
tree,
|
|
449
|
+
context,
|
|
450
|
+
scoreRelevance,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// Find hidden components
|
|
454
|
+
const hiddenComponents = findHiddenComponents(
|
|
455
|
+
tree,
|
|
456
|
+
context,
|
|
457
|
+
scoreRelevance,
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// Predict likely next paths - use custom or default
|
|
461
|
+
const predictions = this.config.signals.predictNavigation
|
|
462
|
+
? this.config.signals.predictNavigation(path, context)
|
|
463
|
+
: defaultPredictNavigation(
|
|
464
|
+
path,
|
|
465
|
+
context,
|
|
466
|
+
this.config.defaultPaths,
|
|
467
|
+
this.config.maxSpeculationPaths,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const { prefetch: prefetchDepth, prerender: prerenderCount } =
|
|
471
|
+
getPrefetchDepth(context);
|
|
472
|
+
|
|
473
|
+
const prefetch = predictions.slice(0, prefetchDepth);
|
|
474
|
+
const prerender = predictions.slice(0, prerenderCount);
|
|
475
|
+
|
|
476
|
+
// Compute skeleton hints
|
|
477
|
+
const skeleton = computeSkeletonHints(path, context, tree);
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
route: path,
|
|
481
|
+
sessionId,
|
|
482
|
+
componentOrder,
|
|
483
|
+
hiddenComponents,
|
|
484
|
+
featureFlags,
|
|
485
|
+
theme,
|
|
486
|
+
accent,
|
|
487
|
+
density,
|
|
488
|
+
prefetch,
|
|
489
|
+
prerender,
|
|
490
|
+
skeleton,
|
|
491
|
+
routedAt: startTime,
|
|
492
|
+
routerName: this.name,
|
|
493
|
+
confidence: 0.85, // Heuristic confidence
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async speculate(
|
|
498
|
+
currentPath: string,
|
|
499
|
+
context: UserContext,
|
|
500
|
+
): Promise<string[]> {
|
|
501
|
+
return this.config.signals.predictNavigation
|
|
502
|
+
? this.config.signals.predictNavigation(currentPath, context)
|
|
503
|
+
: defaultPredictNavigation(
|
|
504
|
+
currentPath,
|
|
505
|
+
context,
|
|
506
|
+
this.config.defaultPaths,
|
|
507
|
+
this.config.maxSpeculationPaths,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
personalizeTree(tree: ComponentTree, decision: RouteDecision): ComponentTree {
|
|
512
|
+
const cloned = tree.clone();
|
|
513
|
+
|
|
514
|
+
// Hide components that should be hidden
|
|
515
|
+
if (decision.hiddenComponents) {
|
|
516
|
+
for (const id of decision.hiddenComponents) {
|
|
517
|
+
const node = cloned.getNode(id);
|
|
518
|
+
if (node) {
|
|
519
|
+
node.defaultHidden = true;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return cloned;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
emotionToAccent(emotionState: EmotionState): string {
|
|
528
|
+
// If app provided a custom deriveAccent, use it
|
|
529
|
+
if (this.config.signals.deriveAccent) {
|
|
530
|
+
return this.config.signals.deriveAccent({
|
|
531
|
+
emotionState,
|
|
532
|
+
// Provide minimal context for just the emotion
|
|
533
|
+
tier: 'free',
|
|
534
|
+
recentPages: [],
|
|
535
|
+
dwellTimes: new Map(),
|
|
536
|
+
clickPatterns: [],
|
|
537
|
+
preferences: {},
|
|
538
|
+
viewport: { width: 0, height: 0 },
|
|
539
|
+
connection: 'fast',
|
|
540
|
+
reducedMotion: false,
|
|
541
|
+
localHour: 12,
|
|
542
|
+
timezone: 'UTC',
|
|
543
|
+
isNewSession: true,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return this.config.defaultAccent;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private generateSessionId(path: string, context: UserContext): string {
|
|
551
|
+
const base = path.replace(/^\/|\/$/g, '').replace(/\//g, '-') || 'index';
|
|
552
|
+
const userId = context.userId || 'anon';
|
|
553
|
+
const sessionPrefix = context.sessionId || Date.now().toString(36);
|
|
554
|
+
|
|
555
|
+
return `${base}-${userId.slice(0, 8)}-${sessionPrefix.slice(0, 8)}`;
|
|
556
|
+
}
|
|
557
|
+
}
|