@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,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
+ }