@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,298 @@
1
+ //! Aeon Router - Fast path matching for Next.js-style routes
2
+ //!
3
+ //! Supports:
4
+ //! - Static routes: /about, /blog
5
+ //! - Dynamic segments: /blog/[slug]
6
+ //! - Catch-all segments: /api/[...path]
7
+ //! - Optional catch-all: /docs/[[...slug]]
8
+ //! - Route groups: (dashboard)/settings (ignored in URL)
9
+
10
+ use wasm_bindgen::prelude::*;
11
+ use std::collections::HashMap;
12
+ use crate::{RouteDefinition, RouteMatch};
13
+
14
+ /// Segment type for route pattern parsing
15
+ #[derive(Clone, Debug, PartialEq)]
16
+ enum Segment {
17
+ /// Static segment like "blog" or "about"
18
+ Static(String),
19
+ /// Dynamic segment like [slug]
20
+ Dynamic(String),
21
+ /// Catch-all segment like [...path]
22
+ CatchAll(String),
23
+ /// Optional catch-all like [[...slug]]
24
+ OptionalCatchAll(String),
25
+ }
26
+
27
+ /// Parsed route pattern
28
+ #[derive(Clone, Debug)]
29
+ struct ParsedRoute {
30
+ segments: Vec<Segment>,
31
+ definition: RouteDefinition,
32
+ }
33
+
34
+ /// The Aeon Router - matches URLs to routes
35
+ #[wasm_bindgen]
36
+ pub struct AeonRouter {
37
+ routes: Vec<ParsedRoute>,
38
+ }
39
+
40
+ #[wasm_bindgen]
41
+ impl AeonRouter {
42
+ #[wasm_bindgen(constructor)]
43
+ pub fn new() -> Self {
44
+ Self { routes: Vec::new() }
45
+ }
46
+
47
+ /// Add a route to the router
48
+ pub fn add_route(&mut self, definition: RouteDefinition) {
49
+ let segments = parse_pattern(&definition.pattern());
50
+ self.routes.push(ParsedRoute {
51
+ segments,
52
+ definition,
53
+ });
54
+ // Sort routes by specificity (static > dynamic > catch-all)
55
+ self.routes.sort_by(|a, b| {
56
+ route_specificity(&b.segments).cmp(&route_specificity(&a.segments))
57
+ });
58
+ }
59
+
60
+ /// Match a URL path to a route
61
+ pub fn match_route(&self, path: &str) -> Option<RouteMatch> {
62
+ let path_segments: Vec<&str> = path
63
+ .trim_start_matches('/')
64
+ .trim_end_matches('/')
65
+ .split('/')
66
+ .filter(|s| !s.is_empty())
67
+ .collect();
68
+
69
+ for parsed in &self.routes {
70
+ if let Some(params) = match_segments(&parsed.segments, &path_segments) {
71
+ let resolved_session_id = resolve_session_id(
72
+ &parsed.definition.session_id(),
73
+ &params,
74
+ );
75
+ return Some(RouteMatch {
76
+ route: parsed.definition.clone(),
77
+ params,
78
+ resolved_session_id,
79
+ });
80
+ }
81
+ }
82
+ None
83
+ }
84
+
85
+ /// Get all registered routes (for debugging)
86
+ pub fn get_routes_json(&self) -> String {
87
+ let patterns: Vec<String> = self.routes
88
+ .iter()
89
+ .map(|r| r.definition.pattern())
90
+ .collect();
91
+ serde_json::to_string(&patterns).unwrap_or_else(|_| "[]".to_string())
92
+ }
93
+
94
+ /// Check if a route exists for the given path
95
+ pub fn has_route(&self, path: &str) -> bool {
96
+ self.match_route(path).is_some()
97
+ }
98
+ }
99
+
100
+ impl Default for AeonRouter {
101
+ fn default() -> Self {
102
+ Self::new()
103
+ }
104
+ }
105
+
106
+ /// Parse a route pattern into segments
107
+ fn parse_pattern(pattern: &str) -> Vec<Segment> {
108
+ pattern
109
+ .trim_start_matches('/')
110
+ .trim_end_matches('/')
111
+ .split('/')
112
+ .filter(|s| !s.is_empty())
113
+ .filter(|s| !is_route_group(s)) // Skip route groups like (dashboard)
114
+ .map(|s| {
115
+ if s.starts_with("[[...") && s.ends_with("]]") {
116
+ // Optional catch-all: [[...slug]]
117
+ let name = s[5..s.len() - 2].to_string();
118
+ Segment::OptionalCatchAll(name)
119
+ } else if s.starts_with("[...") && s.ends_with(']') {
120
+ // Catch-all: [...path]
121
+ let name = s[4..s.len() - 1].to_string();
122
+ Segment::CatchAll(name)
123
+ } else if s.starts_with('[') && s.ends_with(']') {
124
+ // Dynamic: [slug]
125
+ let name = s[1..s.len() - 1].to_string();
126
+ Segment::Dynamic(name)
127
+ } else {
128
+ // Static
129
+ Segment::Static(s.to_string())
130
+ }
131
+ })
132
+ .collect()
133
+ }
134
+
135
+ /// Check if a segment is a route group (parentheses)
136
+ fn is_route_group(segment: &str) -> bool {
137
+ segment.starts_with('(') && segment.ends_with(')')
138
+ }
139
+
140
+ /// Calculate route specificity for sorting (higher = more specific)
141
+ fn route_specificity(segments: &[Segment]) -> usize {
142
+ let mut score = 0;
143
+ for (i, segment) in segments.iter().enumerate() {
144
+ let position_weight = 1000 - i; // Earlier segments are more important
145
+ score += match segment {
146
+ Segment::Static(_) => position_weight * 10,
147
+ Segment::Dynamic(_) => position_weight * 5,
148
+ Segment::CatchAll(_) => 1,
149
+ Segment::OptionalCatchAll(_) => 0,
150
+ };
151
+ }
152
+ score
153
+ }
154
+
155
+ /// Match path segments against route segments, returning extracted params
156
+ fn match_segments(
157
+ route_segments: &[Segment],
158
+ path_segments: &[&str],
159
+ ) -> Option<HashMap<String, String>> {
160
+ let mut params = HashMap::new();
161
+ let mut path_idx = 0;
162
+
163
+ for (_route_idx, segment) in route_segments.iter().enumerate() {
164
+ match segment {
165
+ Segment::Static(expected) => {
166
+ if path_idx >= path_segments.len() {
167
+ return None;
168
+ }
169
+ if path_segments[path_idx] != expected {
170
+ return None;
171
+ }
172
+ path_idx += 1;
173
+ }
174
+ Segment::Dynamic(name) => {
175
+ if path_idx >= path_segments.len() {
176
+ return None;
177
+ }
178
+ params.insert(name.clone(), path_segments[path_idx].to_string());
179
+ path_idx += 1;
180
+ }
181
+ Segment::CatchAll(name) => {
182
+ if path_idx >= path_segments.len() {
183
+ return None; // Catch-all must match at least one segment
184
+ }
185
+ let remaining: Vec<&str> = path_segments[path_idx..].to_vec();
186
+ params.insert(name.clone(), remaining.join("/"));
187
+ path_idx = path_segments.len();
188
+ }
189
+ Segment::OptionalCatchAll(name) => {
190
+ // Optional catch-all can match zero or more segments
191
+ if path_idx < path_segments.len() {
192
+ let remaining: Vec<&str> = path_segments[path_idx..].to_vec();
193
+ params.insert(name.clone(), remaining.join("/"));
194
+ path_idx = path_segments.len();
195
+ }
196
+ // If no more segments, that's fine - it's optional
197
+ }
198
+ }
199
+ }
200
+
201
+ // All path segments must be consumed (unless we had a catch-all)
202
+ if path_idx == path_segments.len() {
203
+ Some(params)
204
+ } else {
205
+ None
206
+ }
207
+ }
208
+
209
+ /// Resolve session ID template with actual params
210
+ fn resolve_session_id(template: &str, params: &HashMap<String, String>) -> String {
211
+ let mut result = template.to_string();
212
+ for (key, value) in params {
213
+ result = result.replace(&format!("${}", key), value);
214
+ }
215
+ result
216
+ }
217
+
218
+ #[cfg(test)]
219
+ mod tests {
220
+ use super::*;
221
+
222
+ #[test]
223
+ fn test_static_route() {
224
+ let mut router = AeonRouter::new();
225
+ router.add_route(RouteDefinition::new(
226
+ "/about".to_string(),
227
+ "about".to_string(),
228
+ "AboutPage".to_string(),
229
+ None,
230
+ false,
231
+ ));
232
+
233
+ let result = router.match_route("/about");
234
+ assert!(result.is_some());
235
+ assert_eq!(result.unwrap().resolved_session_id(), "about");
236
+ }
237
+
238
+ #[test]
239
+ fn test_dynamic_route() {
240
+ let mut router = AeonRouter::new();
241
+ router.add_route(RouteDefinition::new(
242
+ "/blog/[slug]".to_string(),
243
+ "blog-$slug".to_string(),
244
+ "BlogPost".to_string(),
245
+ None,
246
+ true,
247
+ ));
248
+
249
+ let result = router.match_route("/blog/hello-world");
250
+ assert!(result.is_some());
251
+ let m = result.unwrap();
252
+ assert_eq!(m.resolved_session_id(), "blog-hello-world");
253
+ assert_eq!(m.get_param("slug"), Some("hello-world".to_string()));
254
+ }
255
+
256
+ #[test]
257
+ fn test_catch_all_route() {
258
+ let mut router = AeonRouter::new();
259
+ router.add_route(RouteDefinition::new(
260
+ "/api/[...path]".to_string(),
261
+ "api-$path".to_string(),
262
+ "ApiHandler".to_string(),
263
+ None,
264
+ false,
265
+ ));
266
+
267
+ let result = router.match_route("/api/users/123/posts");
268
+ assert!(result.is_some());
269
+ let m = result.unwrap();
270
+ assert_eq!(m.get_param("path"), Some("users/123/posts".to_string()));
271
+ }
272
+
273
+ #[test]
274
+ fn test_route_specificity() {
275
+ let mut router = AeonRouter::new();
276
+
277
+ // Add routes in random order
278
+ router.add_route(RouteDefinition::new(
279
+ "/blog/[slug]".to_string(),
280
+ "blog-$slug".to_string(),
281
+ "BlogPost".to_string(),
282
+ None,
283
+ true,
284
+ ));
285
+ router.add_route(RouteDefinition::new(
286
+ "/blog/featured".to_string(),
287
+ "blog-featured".to_string(),
288
+ "FeaturedPost".to_string(),
289
+ None,
290
+ true,
291
+ ));
292
+
293
+ // Static route should match before dynamic
294
+ let result = router.match_route("/blog/featured");
295
+ assert!(result.is_some());
296
+ assert_eq!(result.unwrap().resolved_session_id(), "blog-featured");
297
+ }
298
+ }