@affectively/aeon-flux 0.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/README.md +438 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +88 -0
- package/examples/basic/components/OfflineIndicator.tsx +93 -0
- package/examples/basic/components/PresenceBar.tsx +68 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +73 -0
- package/package.json +90 -0
- package/packages/benchmarks/src/benchmark.test.ts +644 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +649 -0
- package/packages/cli/src/commands/build.ts +853 -0
- package/packages/cli/src/commands/dev.ts +463 -0
- package/packages/cli/src/commands/init.ts +395 -0
- package/packages/cli/src/commands/start.ts +289 -0
- package/packages/cli/src/index.ts +102 -0
- package/packages/directives/src/use-aeon.ts +266 -0
- package/packages/react/package.json +34 -0
- package/packages/react/src/Link.tsx +355 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +204 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +253 -0
- package/packages/react/src/hooks/useServiceWorker.ts +276 -0
- package/packages/react/src/hooks.ts +192 -0
- package/packages/react/src/index.ts +89 -0
- package/packages/react/src/provider.tsx +428 -0
- package/packages/runtime/package.json +70 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +453 -0
- package/packages/runtime/src/benchmark.ts +145 -0
- package/packages/runtime/src/cache.ts +287 -0
- package/packages/runtime/src/durable-object.ts +847 -0
- package/packages/runtime/src/index.ts +235 -0
- package/packages/runtime/src/navigation.test.ts +432 -0
- package/packages/runtime/src/navigation.ts +412 -0
- package/packages/runtime/src/nextjs-adapter.ts +254 -0
- package/packages/runtime/src/predictor.ts +368 -0
- package/packages/runtime/src/registry.ts +339 -0
- package/packages/runtime/src/router/context-extractor.ts +394 -0
- package/packages/runtime/src/router/esi-control-react.tsx +1172 -0
- package/packages/runtime/src/router/esi-control.ts +488 -0
- package/packages/runtime/src/router/esi-react.tsx +600 -0
- package/packages/runtime/src/router/esi.ts +595 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +272 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +544 -0
- package/packages/runtime/src/router/index.ts +158 -0
- package/packages/runtime/src/router/speculation.ts +442 -0
- package/packages/runtime/src/router/types.ts +514 -0
- package/packages/runtime/src/router.test.ts +466 -0
- package/packages/runtime/src/router.ts +285 -0
- package/packages/runtime/src/server.ts +446 -0
- package/packages/runtime/src/service-worker.ts +418 -0
- package/packages/runtime/src/speculation.test.ts +360 -0
- package/packages/runtime/src/speculation.ts +456 -0
- package/packages/runtime/src/storage.test.ts +1201 -0
- package/packages/runtime/src/storage.ts +1031 -0
- package/packages/runtime/src/tree-compiler.ts +252 -0
- package/packages/runtime/src/types.ts +444 -0
- package/packages/runtime/src/worker.ts +300 -0
- package/packages/runtime/tsconfig.json +19 -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 +328 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1267 -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 +73 -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 +189 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
package/README.md
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# @affectively/aeon-flux
|
|
2
|
+
|
|
3
|
+
**The CMS IS the website.** Collaborative page framework with CRDT-based flux state and hyperpersonalized routing.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
'use aeon'; // One directive enables everything
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What is Aeon Flux?
|
|
10
|
+
|
|
11
|
+
Aeon Flux is a lightweight, collaborative page framework that unifies:
|
|
12
|
+
|
|
13
|
+
- **CMS = Website** - No separate admin panel, edit in place
|
|
14
|
+
- **Editor = Viewer** - Click to edit, changes are live
|
|
15
|
+
- **Backend = Frontend** - One runtime, runs on edge
|
|
16
|
+
- **Pages from D1** - No file system, pure database
|
|
17
|
+
- **AI Everywhere** - Edge Side Inference (ESI) brings AI to any component
|
|
18
|
+
|
|
19
|
+
The name comes from:
|
|
20
|
+
1. **Flux** - CRDT-based data model for conflict-free collaboration
|
|
21
|
+
2. **Aeon Flux** - The classic anime (a syzygy of human and machine)
|
|
22
|
+
|
|
23
|
+
## Key Features
|
|
24
|
+
|
|
25
|
+
| Feature | Description |
|
|
26
|
+
|---------|-------------|
|
|
27
|
+
| **Zero-Dependency Rendering** | Single HTML with inline CSS, assets, fonts |
|
|
28
|
+
| **Hyperpersonalized Routing** | The website comes to the person |
|
|
29
|
+
| **Edge Side Inference (ESI)** | AI inference at render time |
|
|
30
|
+
| **Pages from D1** | No file system in production |
|
|
31
|
+
| **~20KB WASM runtime** | vs 500KB+ Next.js |
|
|
32
|
+
| **Multi-layer caching** | KV (1ms) → D1 (5ms) → Session (50ms) |
|
|
33
|
+
| **Speculative pre-rendering** | Zero-latency navigation |
|
|
34
|
+
| **Built-in collaboration** | Real-time editing with presence |
|
|
35
|
+
| **GitHub PR Publishing** | Visual edits → TSX → Git → Deploy |
|
|
36
|
+
| **Lazy hydration** | Only hydrate interactive components on visibility |
|
|
37
|
+
|
|
38
|
+
## Live Infrastructure
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
https://aeon-flux.taylorbuley.workers.dev
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Endpoint | Description |
|
|
45
|
+
|----------|-------------|
|
|
46
|
+
| `/health` | Health check |
|
|
47
|
+
| `/session/:id` | WebSocket for real-time collab |
|
|
48
|
+
| `/session/:id/session` | Session state (GET/PUT) |
|
|
49
|
+
| `/session/:id/presence` | Active users |
|
|
50
|
+
| `/routes` | Route registry |
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Install
|
|
56
|
+
bun add @affectively/aeon-pages-runtime
|
|
57
|
+
|
|
58
|
+
# Or use the worker directly
|
|
59
|
+
curl https://aeon-flux.taylorbuley.workers.dev/health
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Hyperpersonalized Routing
|
|
63
|
+
|
|
64
|
+
**"The website comes to the person"** - Routes adapt based on user context.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { HeuristicAdapter, extractUserContext } from '@affectively/aeon-pages-runtime';
|
|
68
|
+
|
|
69
|
+
const adapter = new HeuristicAdapter({
|
|
70
|
+
defaultPaths: ['/', '/chat', '/settings'],
|
|
71
|
+
signals: {
|
|
72
|
+
deriveTheme: (ctx) => ctx.localHour > 18 ? 'dark' : 'light',
|
|
73
|
+
deriveAccent: (ctx) => ctx.emotionState?.primary === 'happy' ? '#FFD700' : '#6366f1',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Route decision includes personalization
|
|
78
|
+
const decision = await adapter.route('/dashboard', userContext, componentTree);
|
|
79
|
+
// { theme: 'dark', accent: '#FFD700', prefetch: ['/chat'], density: 'comfortable', ... }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Performance
|
|
83
|
+
|
|
84
|
+
| Operation | 100 nodes | 500 nodes |
|
|
85
|
+
|-----------|-----------|-----------|
|
|
86
|
+
| `route()` | 0.014ms | 0.05ms |
|
|
87
|
+
| `speculate()` | 0.003ms | 0.007ms |
|
|
88
|
+
| `personalizeTree()` | 0.026ms | 0.109ms |
|
|
89
|
+
|
|
90
|
+
Sub-millisecond routing on every request.
|
|
91
|
+
|
|
92
|
+
## Edge Side Inference (ESI)
|
|
93
|
+
|
|
94
|
+
Bring AI to any component at render time. Like Varnish ESI, but for inference.
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { ESI, ESIProvider } from '@affectively/aeon-pages-runtime/router';
|
|
98
|
+
|
|
99
|
+
// Basic inference
|
|
100
|
+
<ESI.Infer model="llm" cache={300}>
|
|
101
|
+
Summarize this page for {user.name}
|
|
102
|
+
</ESI.Infer>
|
|
103
|
+
|
|
104
|
+
// Structured output with Zod
|
|
105
|
+
<ESI.Structured schema={z.object({ sentiment: z.enum(['positive', 'negative']) })}>
|
|
106
|
+
Analyze: {userMessage}
|
|
107
|
+
</ESI.Structured>
|
|
108
|
+
|
|
109
|
+
// Conditional rendering
|
|
110
|
+
<ESI.If
|
|
111
|
+
prompt="Should we show a discount?"
|
|
112
|
+
schema={z.object({ show: z.boolean() })}
|
|
113
|
+
when={(r) => r.show}
|
|
114
|
+
>
|
|
115
|
+
<DiscountBanner />
|
|
116
|
+
</ESI.If>
|
|
117
|
+
|
|
118
|
+
// Presence-aware (adapts for multiple viewers)
|
|
119
|
+
<ESI.Collaborative schema={summarySchema}>
|
|
120
|
+
Summarize for {presence.length} viewers: {content}
|
|
121
|
+
</ESI.Collaborative>
|
|
122
|
+
|
|
123
|
+
// Self-optimizing (improves when user is alone)
|
|
124
|
+
<ESI.Optimize
|
|
125
|
+
schema={contentSchema}
|
|
126
|
+
maxIterations={3}
|
|
127
|
+
goal="Improve clarity and engagement"
|
|
128
|
+
>
|
|
129
|
+
{draftContent}
|
|
130
|
+
</ESI.Optimize>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Zero-Dependency Rendering
|
|
134
|
+
|
|
135
|
+
Every page is a **completely self-contained HTML document**. No external requests needed.
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
Single Request → Instant Render
|
|
139
|
+
├── Inline CSS (tree-shaken, only used classes)
|
|
140
|
+
├── Inline assets (SVG, images as data URIs)
|
|
141
|
+
├── Inline fonts (@font-face with embedded data)
|
|
142
|
+
├── Minimal hydration script (lazy, on visibility)
|
|
143
|
+
└── WASM-rendered at edge (~7ms total)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Multi-Layer Caching
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
Request → KV Cache (1ms) → D1 Cache (5ms) → Session Render (50ms)
|
|
150
|
+
↓ ↓ ↓
|
|
151
|
+
Return HTML Cache to KV Cache to KV + D1
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
All pages are pre-rendered at build time and cached. First request for any route is a cache hit.
|
|
155
|
+
|
|
156
|
+
### Speculative Pre-Rendering
|
|
157
|
+
|
|
158
|
+
Pages are pre-rendered **before the user clicks**, based on:
|
|
159
|
+
- Link visibility (IntersectionObserver)
|
|
160
|
+
- Hover intent
|
|
161
|
+
- Navigation prediction (Markov chain, community patterns)
|
|
162
|
+
- Browser Speculation Rules API
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { initSpeculativeRendering } from '@affectively/aeon-pages-runtime';
|
|
166
|
+
|
|
167
|
+
// Enable instant navigation
|
|
168
|
+
initSpeculativeRendering({
|
|
169
|
+
maxCachedPages: 5,
|
|
170
|
+
prerenderOnHover: true,
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Performance
|
|
175
|
+
|
|
176
|
+
| Metric | Before | After |
|
|
177
|
+
|--------|--------|-------|
|
|
178
|
+
| Requests | 15-30 | 1 |
|
|
179
|
+
| Total bytes | ~655KB | ~110KB |
|
|
180
|
+
| TTFB | 100ms | 50ms |
|
|
181
|
+
| First Paint | 500ms | <100ms |
|
|
182
|
+
| Time to Interactive | 2000ms | <300ms |
|
|
183
|
+
| CLS | 0.05 | 0 |
|
|
184
|
+
|
|
185
|
+
## GitHub PR Publishing
|
|
186
|
+
|
|
187
|
+
Visual edits compile to TSX and create PRs automatically.
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
Edit in browser → Durable Object → "Publish" → TSX → PR → CI deploys
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### WebSocket Commands
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
const ws = new WebSocket('wss://aeon-flux.taylorbuley.workers.dev/session/my-page?userId=user1');
|
|
197
|
+
|
|
198
|
+
// Publish current tree → creates PR
|
|
199
|
+
ws.send(JSON.stringify({ type: 'publish' }));
|
|
200
|
+
// Response: { type: 'publish', payload: { prNumber: 123, autoMerged: false } }
|
|
201
|
+
|
|
202
|
+
// Merge a PR
|
|
203
|
+
ws.send(JSON.stringify({ type: 'merge', payload: { prNumber: 123 } }));
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Generated TSX
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
'use aeon';
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* AboutPage
|
|
213
|
+
* Route: /about
|
|
214
|
+
*
|
|
215
|
+
* @generated by aeon-flux visual editor
|
|
216
|
+
*/
|
|
217
|
+
|
|
218
|
+
import type { FC } from 'react';
|
|
219
|
+
import { Hero } from '@/components/Hero';
|
|
220
|
+
import { Section } from '@/components/Section';
|
|
221
|
+
|
|
222
|
+
const AboutPage: FC = () => {
|
|
223
|
+
return (
|
|
224
|
+
<Page>
|
|
225
|
+
<Hero title="About Us" />
|
|
226
|
+
<Section>
|
|
227
|
+
<Text>We believe in...</Text>
|
|
228
|
+
</Section>
|
|
229
|
+
</Page>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export default AboutPage;
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Configuration
|
|
237
|
+
|
|
238
|
+
```toml
|
|
239
|
+
# wrangler.toml
|
|
240
|
+
[vars]
|
|
241
|
+
GITHUB_REPO = "owner/repo"
|
|
242
|
+
GITHUB_TREE_PATH = "apps/web/pages"
|
|
243
|
+
GITHUB_BASE_BRANCH = "staging"
|
|
244
|
+
GITHUB_DEV_BRANCH = "development"
|
|
245
|
+
GITHUB_AUTO_MERGE = "false"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Production Architecture
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
252
|
+
│ AEON FLUX ARCHITECTURE │
|
|
253
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
254
|
+
│ │
|
|
255
|
+
│ Request + User Context │
|
|
256
|
+
│ │ │
|
|
257
|
+
│ ▼ │
|
|
258
|
+
│ ┌──────────────────┐ ┌──────────────────┐ │
|
|
259
|
+
│ │ HeuristicAdapter │ │ ESI │ │
|
|
260
|
+
│ │ (personalize) │ │ (inference) │ │
|
|
261
|
+
│ └────────┬─────────┘ └────────┬─────────┘ │
|
|
262
|
+
│ │ │ │
|
|
263
|
+
│ ▼ ▼ │
|
|
264
|
+
│ ┌──────────────────────────────────────────────────┐ │
|
|
265
|
+
│ │ Durable Objects │ │
|
|
266
|
+
│ │ (sessions, presence, real-time sync) │ │
|
|
267
|
+
│ └──────────────────────────────────────────────────┘ │
|
|
268
|
+
│ │ │ │
|
|
269
|
+
│ ▼ ▼ │
|
|
270
|
+
│ ┌──────────────┐ ┌──────────────┐ │
|
|
271
|
+
│ │ D1 │ │ GitHub │ │
|
|
272
|
+
│ │ (storage) │ │ (publish) │ │
|
|
273
|
+
│ └──────────────┘ └──────────────┘ │
|
|
274
|
+
│ │
|
|
275
|
+
│ Edit visually → Sync via DO → Publish → TSX → PR → Deploy │
|
|
276
|
+
│ │
|
|
277
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Local-First with Dash
|
|
281
|
+
|
|
282
|
+
Data lives client-side. No backend services needed.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { DashStorageAdapter } from '@affectively/aeon-pages-runtime';
|
|
286
|
+
|
|
287
|
+
const storage = new DashStorageAdapter(dashClient, {
|
|
288
|
+
routesCollection: 'aeon-routes',
|
|
289
|
+
sessionsCollection: 'aeon-sessions',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Reads: instant (local)
|
|
293
|
+
// Writes: instant (local), syncs via DO
|
|
294
|
+
// Collab: Durable Objects
|
|
295
|
+
// AI: ESI at edge
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Speculation & Prefetching
|
|
299
|
+
|
|
300
|
+
Predict and prefetch likely next pages.
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { SpeculationManager, createSpeculationHook } from '@affectively/aeon-pages-runtime/router';
|
|
304
|
+
|
|
305
|
+
// Client-side
|
|
306
|
+
const manager = new SpeculationManager({
|
|
307
|
+
maxPrefetch: 5,
|
|
308
|
+
hoverDelay: 100,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Prefetch on hover
|
|
312
|
+
manager.observeLinks();
|
|
313
|
+
|
|
314
|
+
// Or use the hook
|
|
315
|
+
const useSpeculation = createSpeculationHook(manager);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Supports [Speculation Rules API](https://developer.chrome.com/docs/web-platform/prerender-pages) with link prefetch fallback.
|
|
319
|
+
|
|
320
|
+
## The `'use aeon'` Directive
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
'use aeon';
|
|
324
|
+
|
|
325
|
+
export default function Page() {
|
|
326
|
+
const {
|
|
327
|
+
presence, // Who's viewing/editing
|
|
328
|
+
sync, // Sync state and controls
|
|
329
|
+
version, // Schema version info
|
|
330
|
+
data, // Collaborative data store
|
|
331
|
+
setData, // Update data
|
|
332
|
+
} = useAeonPage();
|
|
333
|
+
|
|
334
|
+
return <div>...</div>;
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Hooks
|
|
339
|
+
|
|
340
|
+
| Hook | Purpose |
|
|
341
|
+
|------|---------|
|
|
342
|
+
| `useAeonPage()` | Full page context |
|
|
343
|
+
| `usePresence()` | Who's here, cursors, editing |
|
|
344
|
+
| `useAeonData<T>(key)` | Collaborative typed data |
|
|
345
|
+
| `useCollaborativeInput(key)` | Ready-to-use collab input |
|
|
346
|
+
| `useOfflineStatus()` | Offline awareness |
|
|
347
|
+
| `useESI()` | ESI context |
|
|
348
|
+
| `useESIInfer()` | Programmatic inference |
|
|
349
|
+
|
|
350
|
+
## Configuration
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// aeon.config.ts
|
|
354
|
+
export default {
|
|
355
|
+
pagesDir: './pages',
|
|
356
|
+
runtime: 'cloudflare',
|
|
357
|
+
|
|
358
|
+
router: {
|
|
359
|
+
adapter: 'heuristic',
|
|
360
|
+
speculation: {
|
|
361
|
+
enabled: true,
|
|
362
|
+
depth: 2,
|
|
363
|
+
prerenderTop: 1,
|
|
364
|
+
},
|
|
365
|
+
personalization: {
|
|
366
|
+
featureGating: true,
|
|
367
|
+
emotionTheming: true,
|
|
368
|
+
componentOrdering: true,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
esi: {
|
|
373
|
+
enabled: true,
|
|
374
|
+
endpoint: process.env.ESI_ENDPOINT,
|
|
375
|
+
timeout: 5000,
|
|
376
|
+
defaultCacheTtl: 300,
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
github: {
|
|
380
|
+
repo: 'owner/repo',
|
|
381
|
+
treePath: 'apps/web/pages',
|
|
382
|
+
baseBranch: 'staging',
|
|
383
|
+
devBranch: 'development',
|
|
384
|
+
autoMerge: false,
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Packages
|
|
390
|
+
|
|
391
|
+
| Package | Description |
|
|
392
|
+
|---------|-------------|
|
|
393
|
+
| `@affectively/aeon-pages-runtime` | Runtime (npm) |
|
|
394
|
+
| `@affectively/aeon-pages-runtime/router` | Personalized routing + ESI |
|
|
395
|
+
| `@affectively/aeon-pages-runtime/server` | Server utilities |
|
|
396
|
+
|
|
397
|
+
## Deploy Your Own
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
cd packages/runtime
|
|
401
|
+
|
|
402
|
+
# Deploy worker
|
|
403
|
+
wrangler deploy
|
|
404
|
+
|
|
405
|
+
# Set GitHub token
|
|
406
|
+
wrangler secret put GITHUB_TOKEN
|
|
407
|
+
|
|
408
|
+
# Create D1 database (optional)
|
|
409
|
+
wrangler d1 create aeon-flux
|
|
410
|
+
wrangler d1 execute aeon-flux --file=./schema.sql
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## vs Next.js
|
|
414
|
+
|
|
415
|
+
| | Next.js | Aeon Flux |
|
|
416
|
+
|-|---------|-----------|
|
|
417
|
+
| **Runtime** | ~500KB | ~187KB |
|
|
418
|
+
| **Routing** | Static | Personalized |
|
|
419
|
+
| **AI** | Add-on | ESI built-in |
|
|
420
|
+
| **CMS** | Separate | It IS the site |
|
|
421
|
+
| **Collaboration** | Add-on | Built-in |
|
|
422
|
+
| **Publish** | Manual | Visual → PR |
|
|
423
|
+
| **Edge** | Partial | Full |
|
|
424
|
+
|
|
425
|
+
## Requirements
|
|
426
|
+
|
|
427
|
+
- Bun >= 1.0.0
|
|
428
|
+
- React >= 18.0.0
|
|
429
|
+
- Zod >= 3.0.0
|
|
430
|
+
- Cloudflare account (for production)
|
|
431
|
+
|
|
432
|
+
## License
|
|
433
|
+
|
|
434
|
+
MIT
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
*The CMS is the website. The website comes to the person. Visual edits become code.*
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AeonConfig } from '@affectively/aeon-pages';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
pagesDir: './pages',
|
|
5
|
+
componentsDir: './components',
|
|
6
|
+
runtime: 'bun',
|
|
7
|
+
port: 3000,
|
|
8
|
+
|
|
9
|
+
aeon: {
|
|
10
|
+
sync: {
|
|
11
|
+
mode: 'distributed',
|
|
12
|
+
replicationFactor: 3,
|
|
13
|
+
consistencyLevel: 'strong',
|
|
14
|
+
},
|
|
15
|
+
versioning: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
autoMigrate: true,
|
|
18
|
+
},
|
|
19
|
+
presence: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
cursorTracking: true,
|
|
22
|
+
inactivityTimeout: 60000,
|
|
23
|
+
},
|
|
24
|
+
offline: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
maxQueueSize: 1000,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
components: {
|
|
31
|
+
autoDiscover: true,
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
nextCompat: true,
|
|
35
|
+
|
|
36
|
+
output: {
|
|
37
|
+
dir: '.aeon',
|
|
38
|
+
},
|
|
39
|
+
} satisfies AeonConfig;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { PresenceUser } from '@affectively/aeon-pages/react';
|
|
2
|
+
|
|
3
|
+
interface CursorProps {
|
|
4
|
+
user: PresenceUser;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Generate a color from user ID
|
|
8
|
+
function userColor(userId: string): string {
|
|
9
|
+
const colors = [
|
|
10
|
+
'#ef4444', // red
|
|
11
|
+
'#f97316', // orange
|
|
12
|
+
'#eab308', // yellow
|
|
13
|
+
'#22c55e', // green
|
|
14
|
+
'#06b6d4', // cyan
|
|
15
|
+
'#3b82f6', // blue
|
|
16
|
+
'#8b5cf6', // violet
|
|
17
|
+
'#ec4899', // pink
|
|
18
|
+
];
|
|
19
|
+
let hash = 0;
|
|
20
|
+
for (let i = 0; i < userId.length; i++) {
|
|
21
|
+
hash = ((hash << 5) - hash) + userId.charCodeAt(i);
|
|
22
|
+
hash = hash & hash;
|
|
23
|
+
}
|
|
24
|
+
return colors[Math.abs(hash) % colors.length];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Cursor({ user }: CursorProps) {
|
|
28
|
+
if (!user.cursor || user.status === 'offline') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const color = userColor(user.userId);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
style={{
|
|
37
|
+
position: 'fixed',
|
|
38
|
+
left: user.cursor.x,
|
|
39
|
+
top: user.cursor.y,
|
|
40
|
+
pointerEvents: 'none',
|
|
41
|
+
zIndex: 9999,
|
|
42
|
+
transition: 'left 0.1s ease-out, top 0.1s ease-out',
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{/* Cursor arrow */}
|
|
46
|
+
<svg
|
|
47
|
+
width="24"
|
|
48
|
+
height="24"
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
fill="none"
|
|
51
|
+
style={{ filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.3))' }}
|
|
52
|
+
>
|
|
53
|
+
<path
|
|
54
|
+
d="M5.5 3.21V20.8c0 .45.54.67.85.35l4.86-4.86a.5.5 0 0 1 .35-.15h6.87c.48 0 .72-.58.38-.92L5.92 2.53a.5.5 0 0 0-.42.68z"
|
|
55
|
+
fill={color}
|
|
56
|
+
stroke="white"
|
|
57
|
+
strokeWidth="1.5"
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
|
|
61
|
+
{/* User label */}
|
|
62
|
+
<div
|
|
63
|
+
style={{
|
|
64
|
+
marginLeft: '16px',
|
|
65
|
+
marginTop: '-4px',
|
|
66
|
+
padding: '2px 8px',
|
|
67
|
+
backgroundColor: color,
|
|
68
|
+
color: 'white',
|
|
69
|
+
fontSize: '12px',
|
|
70
|
+
fontWeight: 500,
|
|
71
|
+
borderRadius: '4px',
|
|
72
|
+
whiteSpace: 'nowrap',
|
|
73
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{user.role === 'assistant' ? '🤖 ' : ''}
|
|
77
|
+
{user.userId.slice(0, 8)}
|
|
78
|
+
{user.editing && (
|
|
79
|
+
<span style={{ marginLeft: '4px', opacity: 0.8 }}>
|
|
80
|
+
editing
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default Cursor;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useOfflineStatus } from '@affectively/aeon-pages/react';
|
|
2
|
+
|
|
3
|
+
export function OfflineIndicator() {
|
|
4
|
+
const { isOffline, isSyncing, pendingOperations, lastSyncAt } = useOfflineStatus();
|
|
5
|
+
|
|
6
|
+
if (!isOffline && !isSyncing && pendingOperations === 0) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="fixed bottom-4 right-4 flex flex-col gap-2">
|
|
12
|
+
{/* Offline banner */}
|
|
13
|
+
{isOffline && (
|
|
14
|
+
<div className="flex items-center gap-2 bg-yellow-50 border border-yellow-200 rounded-lg px-4 py-2 shadow-lg">
|
|
15
|
+
<svg
|
|
16
|
+
className="w-5 h-5 text-yellow-500"
|
|
17
|
+
fill="none"
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
stroke="currentColor"
|
|
20
|
+
>
|
|
21
|
+
<path
|
|
22
|
+
strokeLinecap="round"
|
|
23
|
+
strokeLinejoin="round"
|
|
24
|
+
strokeWidth={2}
|
|
25
|
+
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3"
|
|
26
|
+
/>
|
|
27
|
+
</svg>
|
|
28
|
+
<div>
|
|
29
|
+
<p className="text-sm font-medium text-yellow-800">You're offline</p>
|
|
30
|
+
<p className="text-xs text-yellow-600">Changes will sync when back online</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
|
|
35
|
+
{/* Pending operations */}
|
|
36
|
+
{pendingOperations > 0 && (
|
|
37
|
+
<div className="flex items-center gap-2 bg-blue-50 border border-blue-200 rounded-lg px-4 py-2 shadow-lg">
|
|
38
|
+
<div className="w-5 h-5">
|
|
39
|
+
<svg className="animate-spin text-blue-500" fill="none" viewBox="0 0 24 24">
|
|
40
|
+
<circle
|
|
41
|
+
className="opacity-25"
|
|
42
|
+
cx="12"
|
|
43
|
+
cy="12"
|
|
44
|
+
r="10"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
strokeWidth="4"
|
|
47
|
+
/>
|
|
48
|
+
<path
|
|
49
|
+
className="opacity-75"
|
|
50
|
+
fill="currentColor"
|
|
51
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
52
|
+
/>
|
|
53
|
+
</svg>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<p className="text-sm font-medium text-blue-800">
|
|
57
|
+
{isSyncing ? 'Syncing...' : `${pendingOperations} pending`}
|
|
58
|
+
</p>
|
|
59
|
+
<p className="text-xs text-blue-600">
|
|
60
|
+
{pendingOperations} {pendingOperations === 1 ? 'change' : 'changes'} to sync
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{/* Last sync time */}
|
|
67
|
+
{lastSyncAt && !isOffline && pendingOperations === 0 && (
|
|
68
|
+
<div className="text-xs text-gray-400 text-right">
|
|
69
|
+
Last synced: {formatRelativeTime(lastSyncAt)}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatRelativeTime(isoString: string): string {
|
|
77
|
+
const date = new Date(isoString);
|
|
78
|
+
const now = new Date();
|
|
79
|
+
const diff = now.getTime() - date.getTime();
|
|
80
|
+
|
|
81
|
+
const seconds = Math.floor(diff / 1000);
|
|
82
|
+
const minutes = Math.floor(seconds / 60);
|
|
83
|
+
const hours = Math.floor(minutes / 60);
|
|
84
|
+
|
|
85
|
+
if (seconds < 10) return 'just now';
|
|
86
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
87
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
88
|
+
if (hours < 24) return `${hours}h ago`;
|
|
89
|
+
|
|
90
|
+
return date.toLocaleDateString();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default OfflineIndicator;
|