@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,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aeon Navigation Predictor
|
|
3
|
+
*
|
|
4
|
+
* Predicts where users will navigate next based on:
|
|
5
|
+
* 1. Personal navigation history (Markov chain)
|
|
6
|
+
* 2. Collaborative signals (where is the community going?)
|
|
7
|
+
* 3. Time-based patterns (Monday morning vs Friday afternoon)
|
|
8
|
+
* 4. Content signals (came from search → exploring)
|
|
9
|
+
*
|
|
10
|
+
* The predictor itself is an Aeon entity - it syncs across nodes
|
|
11
|
+
* to build community navigation patterns.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface PredictedRoute {
|
|
15
|
+
route: string;
|
|
16
|
+
probability: number;
|
|
17
|
+
reason: 'history' | 'hover' | 'visibility' | 'community' | 'time' | 'content';
|
|
18
|
+
confidence: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NavigationRecord {
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
timestamp: number;
|
|
25
|
+
duration: number; // How long they stayed on 'from'
|
|
26
|
+
source?: 'click' | 'back' | 'forward' | 'direct';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CommunityPattern {
|
|
30
|
+
route: string;
|
|
31
|
+
popularity: number; // How many users visited
|
|
32
|
+
avgTimeSpent: number;
|
|
33
|
+
nextRoutes: { route: string; count: number }[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PredictorConfig {
|
|
37
|
+
historyWeight: number; // 0-1, weight for personal history
|
|
38
|
+
communityWeight: number; // 0-1, weight for community patterns
|
|
39
|
+
timeWeight: number; // 0-1, weight for time-based patterns
|
|
40
|
+
decayFactor: number; // How quickly old history decays (0-1)
|
|
41
|
+
minProbability: number; // Minimum probability to include in predictions
|
|
42
|
+
maxPredictions: number; // Maximum predictions to return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_CONFIG: PredictorConfig = {
|
|
46
|
+
historyWeight: 0.5,
|
|
47
|
+
communityWeight: 0.3,
|
|
48
|
+
timeWeight: 0.2,
|
|
49
|
+
decayFactor: 0.95,
|
|
50
|
+
minProbability: 0.1,
|
|
51
|
+
maxPredictions: 5,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export class NavigationPredictor {
|
|
55
|
+
private config: PredictorConfig;
|
|
56
|
+
private history: NavigationRecord[] = [];
|
|
57
|
+
private transitionMatrix: Map<string, Map<string, number>> = new Map();
|
|
58
|
+
private communityPatterns: Map<string, CommunityPattern> = new Map();
|
|
59
|
+
private timePatterns: Map<string, Map<number, number>> = new Map(); // route -> hour -> count
|
|
60
|
+
|
|
61
|
+
constructor(config: Partial<PredictorConfig> = {}) {
|
|
62
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Record a navigation event
|
|
67
|
+
*/
|
|
68
|
+
record(record: NavigationRecord): void {
|
|
69
|
+
this.history.push(record);
|
|
70
|
+
|
|
71
|
+
// Update transition matrix
|
|
72
|
+
if (!this.transitionMatrix.has(record.from)) {
|
|
73
|
+
this.transitionMatrix.set(record.from, new Map());
|
|
74
|
+
}
|
|
75
|
+
const fromMap = this.transitionMatrix.get(record.from)!;
|
|
76
|
+
fromMap.set(record.to, (fromMap.get(record.to) ?? 0) + 1);
|
|
77
|
+
|
|
78
|
+
// Update time patterns
|
|
79
|
+
const hour = new Date(record.timestamp).getHours();
|
|
80
|
+
if (!this.timePatterns.has(record.to)) {
|
|
81
|
+
this.timePatterns.set(record.to, new Map());
|
|
82
|
+
}
|
|
83
|
+
const hourMap = this.timePatterns.get(record.to)!;
|
|
84
|
+
hourMap.set(hour, (hourMap.get(hour) ?? 0) + 1);
|
|
85
|
+
|
|
86
|
+
// Apply decay to old records
|
|
87
|
+
this.applyDecay();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Predict next navigation destinations from current route
|
|
92
|
+
*/
|
|
93
|
+
predict(currentRoute: string): PredictedRoute[] {
|
|
94
|
+
const predictions: Map<string, PredictedRoute> = new Map();
|
|
95
|
+
|
|
96
|
+
// 1. Personal history predictions (Markov chain)
|
|
97
|
+
const historyPredictions = this.predictFromHistory(currentRoute);
|
|
98
|
+
for (const pred of historyPredictions) {
|
|
99
|
+
this.mergePrediction(predictions, pred, this.config.historyWeight);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Community predictions
|
|
103
|
+
const communityPredictions = this.predictFromCommunity(currentRoute);
|
|
104
|
+
for (const pred of communityPredictions) {
|
|
105
|
+
this.mergePrediction(predictions, pred, this.config.communityWeight);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Time-based predictions
|
|
109
|
+
const timePredictions = this.predictFromTime();
|
|
110
|
+
for (const pred of timePredictions) {
|
|
111
|
+
this.mergePrediction(predictions, pred, this.config.timeWeight);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Sort by probability and filter
|
|
115
|
+
return Array.from(predictions.values())
|
|
116
|
+
.filter((p) => p.probability >= this.config.minProbability)
|
|
117
|
+
.sort((a, b) => b.probability - a.probability)
|
|
118
|
+
.slice(0, this.config.maxPredictions);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Predict from personal navigation history (Markov chain)
|
|
123
|
+
*/
|
|
124
|
+
private predictFromHistory(currentRoute: string): PredictedRoute[] {
|
|
125
|
+
const fromMap = this.transitionMatrix.get(currentRoute);
|
|
126
|
+
if (!fromMap) return [];
|
|
127
|
+
|
|
128
|
+
const total = Array.from(fromMap.values()).reduce((a, b) => a + b, 0);
|
|
129
|
+
if (total === 0) return [];
|
|
130
|
+
|
|
131
|
+
return Array.from(fromMap.entries()).map(([route, count]) => ({
|
|
132
|
+
route,
|
|
133
|
+
probability: count / total,
|
|
134
|
+
reason: 'history' as const,
|
|
135
|
+
confidence: Math.min(1, total / 10), // Higher confidence with more data
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Predict from community patterns
|
|
141
|
+
*/
|
|
142
|
+
private predictFromCommunity(currentRoute: string): PredictedRoute[] {
|
|
143
|
+
const pattern = this.communityPatterns.get(currentRoute);
|
|
144
|
+
if (!pattern || pattern.nextRoutes.length === 0) return [];
|
|
145
|
+
|
|
146
|
+
const total = pattern.nextRoutes.reduce((a, b) => a + b.count, 0);
|
|
147
|
+
if (total === 0) return [];
|
|
148
|
+
|
|
149
|
+
return pattern.nextRoutes.map(({ route, count }) => ({
|
|
150
|
+
route,
|
|
151
|
+
probability: count / total,
|
|
152
|
+
reason: 'community' as const,
|
|
153
|
+
confidence: Math.min(1, pattern.popularity / 100),
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Predict from time-based patterns
|
|
159
|
+
*/
|
|
160
|
+
private predictFromTime(): PredictedRoute[] {
|
|
161
|
+
const currentHour = new Date().getHours();
|
|
162
|
+
const predictions: PredictedRoute[] = [];
|
|
163
|
+
|
|
164
|
+
let maxCount = 0;
|
|
165
|
+
for (const [route, hourMap] of this.timePatterns) {
|
|
166
|
+
const count = hourMap.get(currentHour) ?? 0;
|
|
167
|
+
if (count > maxCount) maxCount = count;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (maxCount === 0) return [];
|
|
171
|
+
|
|
172
|
+
for (const [route, hourMap] of this.timePatterns) {
|
|
173
|
+
const count = hourMap.get(currentHour) ?? 0;
|
|
174
|
+
if (count > 0) {
|
|
175
|
+
predictions.push({
|
|
176
|
+
route,
|
|
177
|
+
probability: count / maxCount,
|
|
178
|
+
reason: 'time' as const,
|
|
179
|
+
confidence: Math.min(1, count / 5),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return predictions;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Merge a prediction into the predictions map
|
|
189
|
+
*/
|
|
190
|
+
private mergePrediction(
|
|
191
|
+
predictions: Map<string, PredictedRoute>,
|
|
192
|
+
prediction: PredictedRoute,
|
|
193
|
+
weight: number,
|
|
194
|
+
): void {
|
|
195
|
+
const existing = predictions.get(prediction.route);
|
|
196
|
+
if (existing) {
|
|
197
|
+
// Combine probabilities (weighted average)
|
|
198
|
+
const totalWeight =
|
|
199
|
+
(existing.confidence ?? 1) + (prediction.confidence ?? 1) * weight;
|
|
200
|
+
existing.probability =
|
|
201
|
+
(existing.probability * (existing.confidence ?? 1) +
|
|
202
|
+
prediction.probability * (prediction.confidence ?? 1) * weight) /
|
|
203
|
+
totalWeight;
|
|
204
|
+
existing.confidence = Math.max(
|
|
205
|
+
existing.confidence,
|
|
206
|
+
prediction.confidence,
|
|
207
|
+
);
|
|
208
|
+
// Keep the higher-confidence reason
|
|
209
|
+
if (prediction.confidence > (existing.confidence ?? 0)) {
|
|
210
|
+
existing.reason = prediction.reason;
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
predictions.set(prediction.route, {
|
|
214
|
+
...prediction,
|
|
215
|
+
probability: prediction.probability * weight,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Apply decay to old history records
|
|
222
|
+
*/
|
|
223
|
+
private applyDecay(): void {
|
|
224
|
+
// Decay transition matrix
|
|
225
|
+
for (const [from, toMap] of this.transitionMatrix) {
|
|
226
|
+
for (const [to, count] of toMap) {
|
|
227
|
+
const decayed = count * this.config.decayFactor;
|
|
228
|
+
if (decayed < 0.1) {
|
|
229
|
+
toMap.delete(to);
|
|
230
|
+
} else {
|
|
231
|
+
toMap.set(to, decayed);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (toMap.size === 0) {
|
|
235
|
+
this.transitionMatrix.delete(from);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Trim old history
|
|
240
|
+
const maxHistory = 1000;
|
|
241
|
+
if (this.history.length > maxHistory) {
|
|
242
|
+
this.history = this.history.slice(-maxHistory);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Update community patterns from external sync
|
|
248
|
+
*/
|
|
249
|
+
updateCommunityPatterns(patterns: Map<string, CommunityPattern>): void {
|
|
250
|
+
this.communityPatterns = patterns;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get current transition matrix (for syncing)
|
|
255
|
+
*/
|
|
256
|
+
getTransitionMatrix(): Map<string, Map<string, number>> {
|
|
257
|
+
return this.transitionMatrix;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Import transition matrix from sync
|
|
262
|
+
*/
|
|
263
|
+
importTransitionMatrix(matrix: Map<string, Map<string, number>>): void {
|
|
264
|
+
// Merge with existing
|
|
265
|
+
for (const [from, toMap] of matrix) {
|
|
266
|
+
if (!this.transitionMatrix.has(from)) {
|
|
267
|
+
this.transitionMatrix.set(from, new Map());
|
|
268
|
+
}
|
|
269
|
+
const existingMap = this.transitionMatrix.get(from)!;
|
|
270
|
+
for (const [to, count] of toMap) {
|
|
271
|
+
existingMap.set(to, (existingMap.get(to) ?? 0) + count);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get statistics about the predictor
|
|
278
|
+
*/
|
|
279
|
+
getStats(): {
|
|
280
|
+
totalRecords: number;
|
|
281
|
+
uniqueRoutes: number;
|
|
282
|
+
transitionPairs: number;
|
|
283
|
+
communityPatterns: number;
|
|
284
|
+
} {
|
|
285
|
+
let transitionPairs = 0;
|
|
286
|
+
for (const toMap of this.transitionMatrix.values()) {
|
|
287
|
+
transitionPairs += toMap.size;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
totalRecords: this.history.length,
|
|
292
|
+
uniqueRoutes: this.transitionMatrix.size,
|
|
293
|
+
transitionPairs,
|
|
294
|
+
communityPatterns: this.communityPatterns.size,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Clear all data
|
|
300
|
+
*/
|
|
301
|
+
clear(): void {
|
|
302
|
+
this.history = [];
|
|
303
|
+
this.transitionMatrix.clear();
|
|
304
|
+
this.communityPatterns.clear();
|
|
305
|
+
this.timePatterns.clear();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Export for persistence
|
|
310
|
+
*/
|
|
311
|
+
export(): {
|
|
312
|
+
history: NavigationRecord[];
|
|
313
|
+
transitionMatrix: [string, [string, number][]][];
|
|
314
|
+
timePatterns: [string, [number, number][]][];
|
|
315
|
+
} {
|
|
316
|
+
return {
|
|
317
|
+
history: this.history,
|
|
318
|
+
transitionMatrix: Array.from(this.transitionMatrix.entries()).map(
|
|
319
|
+
([from, toMap]) => [from, Array.from(toMap.entries())],
|
|
320
|
+
),
|
|
321
|
+
timePatterns: Array.from(this.timePatterns.entries()).map(
|
|
322
|
+
([route, hourMap]) => [route, Array.from(hourMap.entries())],
|
|
323
|
+
),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Import from persistence
|
|
329
|
+
*/
|
|
330
|
+
import(data: {
|
|
331
|
+
history?: NavigationRecord[];
|
|
332
|
+
transitionMatrix?: [string, [string, number][]][];
|
|
333
|
+
timePatterns?: [string, [number, number][]][];
|
|
334
|
+
}): void {
|
|
335
|
+
if (data.history) {
|
|
336
|
+
this.history = data.history;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (data.transitionMatrix) {
|
|
340
|
+
this.transitionMatrix = new Map(
|
|
341
|
+
data.transitionMatrix.map(([from, toEntries]) => [
|
|
342
|
+
from,
|
|
343
|
+
new Map(toEntries),
|
|
344
|
+
]),
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (data.timePatterns) {
|
|
349
|
+
this.timePatterns = new Map(
|
|
350
|
+
data.timePatterns.map(([route, hourEntries]) => [
|
|
351
|
+
route,
|
|
352
|
+
new Map(hourEntries),
|
|
353
|
+
]),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Singleton instance
|
|
360
|
+
let globalPredictor: NavigationPredictor | null = null;
|
|
361
|
+
|
|
362
|
+
export function getPredictor(): NavigationPredictor {
|
|
363
|
+
if (!globalPredictor) {
|
|
364
|
+
globalPredictor = new NavigationPredictor();
|
|
365
|
+
}
|
|
366
|
+
return globalPredictor;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function setPredictor(predictor: NavigationPredictor): void {
|
|
370
|
+
globalPredictor = predictor;
|
|
371
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aeon Route Registry - Collaborative route management
|
|
3
|
+
*
|
|
4
|
+
* Routes are stored as Aeon entities, enabling:
|
|
5
|
+
* - Distributed sync across nodes
|
|
6
|
+
* - Conflict resolution for concurrent route mutations
|
|
7
|
+
* - Schema versioning for route structure changes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RouteDefinition, RouteMetadata, RouteOperation } from './types';
|
|
11
|
+
|
|
12
|
+
// Import Aeon modules (these would come from @affectively/aeon)
|
|
13
|
+
// For now, we'll define minimal interfaces to compile
|
|
14
|
+
interface SyncCoordinatorLike {
|
|
15
|
+
getLocalNodeId(): string;
|
|
16
|
+
getOnlineNodes(): { id: string }[];
|
|
17
|
+
createSyncSession(initiator: string, participants: string[]): Promise<void>;
|
|
18
|
+
on(event: string, callback: (data: unknown) => void): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface StateReconcilerLike {
|
|
22
|
+
recordVersion(nodeId: string, state: unknown, timestamp: number): void;
|
|
23
|
+
reconcile(): { state: unknown } | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SchemaVersionManagerLike {
|
|
27
|
+
getCurrentVersion(): Promise<string>;
|
|
28
|
+
registerVersion(version: string, metadata: { description: string }): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface RegistryOptions {
|
|
32
|
+
syncMode: 'distributed' | 'local';
|
|
33
|
+
versioningEnabled: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface AeonRoute {
|
|
37
|
+
path: string;
|
|
38
|
+
component: string;
|
|
39
|
+
metadata: RouteMetadata;
|
|
40
|
+
version: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Collaborative route registry using Aeon distributed sync
|
|
45
|
+
*/
|
|
46
|
+
export class AeonRouteRegistry {
|
|
47
|
+
private routes: Map<string, AeonRoute> = new Map();
|
|
48
|
+
private coordinator: SyncCoordinatorLike | null = null;
|
|
49
|
+
private reconciler: StateReconcilerLike | null = null;
|
|
50
|
+
private versions: SchemaVersionManagerLike | null = null;
|
|
51
|
+
private syncMode: 'distributed' | 'local';
|
|
52
|
+
private versioningEnabled: boolean;
|
|
53
|
+
private mutationCallbacks: ((operation: RouteOperation) => void)[] = [];
|
|
54
|
+
private connectedSockets: Set<unknown> = new Set();
|
|
55
|
+
|
|
56
|
+
constructor(options: RegistryOptions) {
|
|
57
|
+
this.syncMode = options.syncMode;
|
|
58
|
+
this.versioningEnabled = options.versioningEnabled;
|
|
59
|
+
|
|
60
|
+
// Initialize Aeon modules (lazy loading to avoid circular deps)
|
|
61
|
+
this.initializeAeonModules();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async initializeAeonModules(): Promise<void> {
|
|
65
|
+
try {
|
|
66
|
+
// Try to import Aeon modules
|
|
67
|
+
const aeon = await import('@affectively/aeon');
|
|
68
|
+
|
|
69
|
+
if (this.syncMode === 'distributed') {
|
|
70
|
+
this.coordinator =
|
|
71
|
+
new aeon.SyncCoordinator() as unknown as SyncCoordinatorLike;
|
|
72
|
+
this.reconciler =
|
|
73
|
+
new aeon.StateReconciler() as unknown as StateReconcilerLike;
|
|
74
|
+
|
|
75
|
+
// Subscribe to sync events
|
|
76
|
+
this.coordinator.on('sync-completed', (session: unknown) => {
|
|
77
|
+
this.handleSyncCompleted(session);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this.versioningEnabled) {
|
|
82
|
+
this.versions =
|
|
83
|
+
new aeon.SchemaVersionManager() as unknown as SchemaVersionManagerLike;
|
|
84
|
+
this.versions.registerVersion('1.0.0', {
|
|
85
|
+
description: 'Initial route schema',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.warn(
|
|
90
|
+
'[aeon-registry] Aeon modules not available, running in standalone mode',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Add a new route collaboratively
|
|
97
|
+
*/
|
|
98
|
+
async addRoute(
|
|
99
|
+
path: string,
|
|
100
|
+
component: string,
|
|
101
|
+
metadata: RouteMetadata,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const operation: RouteOperation = {
|
|
104
|
+
type: 'route-add',
|
|
105
|
+
path,
|
|
106
|
+
component,
|
|
107
|
+
metadata,
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
nodeId: this.coordinator?.getLocalNodeId() ?? 'local',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Sync with other nodes if in distributed mode
|
|
113
|
+
if (this.syncMode === 'distributed' && this.coordinator) {
|
|
114
|
+
const participants = this.coordinator.getOnlineNodes().map((n) => n.id);
|
|
115
|
+
if (participants.length > 0) {
|
|
116
|
+
await this.coordinator.createSyncSession(
|
|
117
|
+
this.coordinator.getLocalNodeId(),
|
|
118
|
+
participants,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Apply locally
|
|
124
|
+
const version =
|
|
125
|
+
this.versioningEnabled && this.versions
|
|
126
|
+
? await this.versions.getCurrentVersion()
|
|
127
|
+
: '1.0.0';
|
|
128
|
+
|
|
129
|
+
this.routes.set(path, {
|
|
130
|
+
path,
|
|
131
|
+
component,
|
|
132
|
+
metadata,
|
|
133
|
+
version,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Notify listeners
|
|
137
|
+
this.notifyMutation(operation);
|
|
138
|
+
|
|
139
|
+
// Persist to file system
|
|
140
|
+
await this.persistRoute(path, component);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Update an existing route
|
|
145
|
+
*/
|
|
146
|
+
async updateRoute(path: string, updates: Partial<AeonRoute>): Promise<void> {
|
|
147
|
+
const existing = this.routes.get(path);
|
|
148
|
+
if (!existing) {
|
|
149
|
+
throw new Error(`Route not found: ${path}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const operation: RouteOperation = {
|
|
153
|
+
type: 'route-update',
|
|
154
|
+
path,
|
|
155
|
+
component: updates.component,
|
|
156
|
+
metadata: {
|
|
157
|
+
...existing.metadata,
|
|
158
|
+
updatedAt: new Date().toISOString(),
|
|
159
|
+
updatedBy: this.coordinator?.getLocalNodeId() ?? 'local',
|
|
160
|
+
},
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
nodeId: this.coordinator?.getLocalNodeId() ?? 'local',
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
this.routes.set(path, {
|
|
166
|
+
...existing,
|
|
167
|
+
...updates,
|
|
168
|
+
metadata: operation.metadata!,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this.notifyMutation(operation);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Remove a route
|
|
176
|
+
*/
|
|
177
|
+
async removeRoute(path: string): Promise<void> {
|
|
178
|
+
const operation: RouteOperation = {
|
|
179
|
+
type: 'route-remove',
|
|
180
|
+
path,
|
|
181
|
+
timestamp: new Date().toISOString(),
|
|
182
|
+
nodeId: this.coordinator?.getLocalNodeId() ?? 'local',
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
this.routes.delete(path);
|
|
186
|
+
this.notifyMutation(operation);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get a route by path
|
|
191
|
+
*/
|
|
192
|
+
getRoute(path: string): AeonRoute | undefined {
|
|
193
|
+
return this.routes.get(path);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get session ID for a path
|
|
198
|
+
*/
|
|
199
|
+
getSessionId(path: string): string {
|
|
200
|
+
return path.replace(/^\/|\/$/g, '').replace(/\//g, '-') || 'index';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get all routes
|
|
205
|
+
*/
|
|
206
|
+
getAllRoutes(): AeonRoute[] {
|
|
207
|
+
return Array.from(this.routes.values());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Subscribe to route mutations
|
|
212
|
+
*/
|
|
213
|
+
subscribeToMutations(
|
|
214
|
+
callback: (operation: RouteOperation) => void,
|
|
215
|
+
): () => void {
|
|
216
|
+
this.mutationCallbacks.push(callback);
|
|
217
|
+
return () => {
|
|
218
|
+
const idx = this.mutationCallbacks.indexOf(callback);
|
|
219
|
+
if (idx >= 0) {
|
|
220
|
+
this.mutationCallbacks.splice(idx, 1);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Handle WebSocket connection for Aeon sync
|
|
227
|
+
*/
|
|
228
|
+
handleConnect(ws: unknown): void {
|
|
229
|
+
this.connectedSockets.add(ws);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Handle WebSocket disconnection
|
|
234
|
+
*/
|
|
235
|
+
handleDisconnect(ws: unknown): void {
|
|
236
|
+
this.connectedSockets.delete(ws);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Handle incoming sync message
|
|
241
|
+
*/
|
|
242
|
+
handleSyncMessage(ws: unknown, message: unknown): void {
|
|
243
|
+
// Parse and apply sync message
|
|
244
|
+
try {
|
|
245
|
+
const data = typeof message === 'string' ? JSON.parse(message) : message;
|
|
246
|
+
|
|
247
|
+
if (data.type === 'route-operation') {
|
|
248
|
+
this.applyRemoteOperation(data.operation as RouteOperation);
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error('[aeon-registry] Error handling sync message:', error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Private methods
|
|
256
|
+
|
|
257
|
+
private notifyMutation(operation: RouteOperation): void {
|
|
258
|
+
for (const callback of this.mutationCallbacks) {
|
|
259
|
+
try {
|
|
260
|
+
callback(operation);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('[aeon-registry] Error in mutation callback:', error);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Broadcast to connected sockets
|
|
267
|
+
const message = JSON.stringify({ type: 'route-operation', operation });
|
|
268
|
+
for (const ws of this.connectedSockets) {
|
|
269
|
+
try {
|
|
270
|
+
// @ts-expect-error - WebSocket send method
|
|
271
|
+
ws.send?.(message);
|
|
272
|
+
} catch {
|
|
273
|
+
// Ignore send errors
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private handleSyncCompleted(session: unknown): void {
|
|
279
|
+
// Apply reconciled state from sync session
|
|
280
|
+
if (this.reconciler) {
|
|
281
|
+
const result = this.reconciler.reconcile();
|
|
282
|
+
if (result?.state) {
|
|
283
|
+
// Apply reconciled routes
|
|
284
|
+
const routes = result.state as Map<string, AeonRoute>;
|
|
285
|
+
for (const [path, route] of routes) {
|
|
286
|
+
this.routes.set(path, route);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private applyRemoteOperation(operation: RouteOperation): void {
|
|
293
|
+
switch (operation.type) {
|
|
294
|
+
case 'route-add':
|
|
295
|
+
if (operation.component && operation.metadata) {
|
|
296
|
+
this.routes.set(operation.path, {
|
|
297
|
+
path: operation.path,
|
|
298
|
+
component: operation.component,
|
|
299
|
+
metadata: operation.metadata,
|
|
300
|
+
version: '1.0.0',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
|
|
305
|
+
case 'route-update':
|
|
306
|
+
const existing = this.routes.get(operation.path);
|
|
307
|
+
if (existing && operation.component) {
|
|
308
|
+
this.routes.set(operation.path, {
|
|
309
|
+
...existing,
|
|
310
|
+
component: operation.component,
|
|
311
|
+
metadata: operation.metadata ?? existing.metadata,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
case 'route-remove':
|
|
317
|
+
this.routes.delete(operation.path);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Notify local listeners
|
|
322
|
+
for (const callback of this.mutationCallbacks) {
|
|
323
|
+
try {
|
|
324
|
+
callback(operation);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error('[aeon-registry] Error in mutation callback:', error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async persistRoute(path: string, component: string): Promise<void> {
|
|
332
|
+
// Convert path to file path
|
|
333
|
+
const filePath = path === '/' ? 'page.tsx' : `${path.slice(1)}/page.tsx`;
|
|
334
|
+
|
|
335
|
+
// Generate minimal page content
|
|
336
|
+
const content = `'use aeon';
|
|
337
|
+
|
|
338
|
+
export default function Page() {
|
|
339
|
+
return <${component} />;
|
|
340
|
+
}
|
|
341
|
+
`;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
// This would write to the file system
|
|
345
|
+
// In production, this would be gated by permissions
|
|
346
|
+
console.log(`[aeon-registry] Would persist route to: ${filePath}`);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error(`[aeon-registry] Error persisting route:`, error);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|