@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.
Files changed (72) hide show
  1. package/README.md +438 -0
  2. package/examples/basic/aeon.config.ts +39 -0
  3. package/examples/basic/components/Cursor.tsx +88 -0
  4. package/examples/basic/components/OfflineIndicator.tsx +93 -0
  5. package/examples/basic/components/PresenceBar.tsx +68 -0
  6. package/examples/basic/package.json +20 -0
  7. package/examples/basic/pages/index.tsx +73 -0
  8. package/package.json +90 -0
  9. package/packages/benchmarks/src/benchmark.test.ts +644 -0
  10. package/packages/cli/package.json +43 -0
  11. package/packages/cli/src/commands/build.test.ts +649 -0
  12. package/packages/cli/src/commands/build.ts +853 -0
  13. package/packages/cli/src/commands/dev.ts +463 -0
  14. package/packages/cli/src/commands/init.ts +395 -0
  15. package/packages/cli/src/commands/start.ts +289 -0
  16. package/packages/cli/src/index.ts +102 -0
  17. package/packages/directives/src/use-aeon.ts +266 -0
  18. package/packages/react/package.json +34 -0
  19. package/packages/react/src/Link.tsx +355 -0
  20. package/packages/react/src/hooks/useAeonNavigation.ts +204 -0
  21. package/packages/react/src/hooks/usePilotNavigation.ts +253 -0
  22. package/packages/react/src/hooks/useServiceWorker.ts +276 -0
  23. package/packages/react/src/hooks.ts +192 -0
  24. package/packages/react/src/index.ts +89 -0
  25. package/packages/react/src/provider.tsx +428 -0
  26. package/packages/runtime/package.json +70 -0
  27. package/packages/runtime/schema.sql +40 -0
  28. package/packages/runtime/src/api-routes.ts +453 -0
  29. package/packages/runtime/src/benchmark.ts +145 -0
  30. package/packages/runtime/src/cache.ts +287 -0
  31. package/packages/runtime/src/durable-object.ts +847 -0
  32. package/packages/runtime/src/index.ts +235 -0
  33. package/packages/runtime/src/navigation.test.ts +432 -0
  34. package/packages/runtime/src/navigation.ts +412 -0
  35. package/packages/runtime/src/nextjs-adapter.ts +254 -0
  36. package/packages/runtime/src/predictor.ts +368 -0
  37. package/packages/runtime/src/registry.ts +339 -0
  38. package/packages/runtime/src/router/context-extractor.ts +394 -0
  39. package/packages/runtime/src/router/esi-control-react.tsx +1172 -0
  40. package/packages/runtime/src/router/esi-control.ts +488 -0
  41. package/packages/runtime/src/router/esi-react.tsx +600 -0
  42. package/packages/runtime/src/router/esi.ts +595 -0
  43. package/packages/runtime/src/router/heuristic-adapter.test.ts +272 -0
  44. package/packages/runtime/src/router/heuristic-adapter.ts +544 -0
  45. package/packages/runtime/src/router/index.ts +158 -0
  46. package/packages/runtime/src/router/speculation.ts +442 -0
  47. package/packages/runtime/src/router/types.ts +514 -0
  48. package/packages/runtime/src/router.test.ts +466 -0
  49. package/packages/runtime/src/router.ts +285 -0
  50. package/packages/runtime/src/server.ts +446 -0
  51. package/packages/runtime/src/service-worker.ts +418 -0
  52. package/packages/runtime/src/speculation.test.ts +360 -0
  53. package/packages/runtime/src/speculation.ts +456 -0
  54. package/packages/runtime/src/storage.test.ts +1201 -0
  55. package/packages/runtime/src/storage.ts +1031 -0
  56. package/packages/runtime/src/tree-compiler.ts +252 -0
  57. package/packages/runtime/src/types.ts +444 -0
  58. package/packages/runtime/src/worker.ts +300 -0
  59. package/packages/runtime/tsconfig.json +19 -0
  60. package/packages/runtime/wrangler.toml +41 -0
  61. package/packages/runtime-wasm/Cargo.lock +436 -0
  62. package/packages/runtime-wasm/Cargo.toml +29 -0
  63. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +328 -0
  64. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1267 -0
  65. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  66. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +73 -0
  67. package/packages/runtime-wasm/pkg/package.json +21 -0
  68. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  69. package/packages/runtime-wasm/src/lib.rs +189 -0
  70. package/packages/runtime-wasm/src/render.rs +629 -0
  71. package/packages/runtime-wasm/src/router.rs +298 -0
  72. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @affectively/aeon-pages/react
3
+ *
4
+ * React bindings for Aeon Pages - collaborative editing with hooks.
5
+ *
6
+ * The Aeon architecture is recursive (fractal):
7
+ * - Component = Aeon entity
8
+ * - Page = Aeon session
9
+ * - Site = Aeon of sessions (routes are collaborative)
10
+ * - Federation = Aeon of Aeons (cross-site sync)
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * 'use aeon';
15
+ *
16
+ * import { Link, useAeonPage, usePresence, useAeonData } from '@affectively/aeon-pages/react';
17
+ *
18
+ * export default function Page() {
19
+ * const { presence, localUser, updateCursor } = usePresence();
20
+ * const [title, setTitle] = useAeonData<string>('title');
21
+ *
22
+ * return (
23
+ * <div onMouseMove={(e) => updateCursor({ x: e.clientX, y: e.clientY })}>
24
+ * <h1 contentEditable onInput={(e) => setTitle(e.currentTarget.textContent)}>
25
+ * {title || 'Untitled'}
26
+ * </h1>
27
+ *
28
+ * <Link href="/about" prefetch="visible" showPresence>
29
+ * About (3 viewing)
30
+ * </Link>
31
+ *
32
+ * {presence.map((user) => (
33
+ * <Cursor key={user.userId} user={user} />
34
+ * ))}
35
+ * </div>
36
+ * );
37
+ * }
38
+ * ```
39
+ */
40
+
41
+ // Link component with prefetch/transitions/presence
42
+ export { Link, type LinkProps, type TransitionType, type PrefetchStrategy, type PresenceRenderProps } from './Link';
43
+
44
+ // Provider and main hook
45
+ export {
46
+ AeonPageProvider,
47
+ useAeonPage,
48
+ type AeonPageProviderProps,
49
+ type AeonPageContextValue,
50
+ type PresenceUser,
51
+ type SyncState,
52
+ type VersionInfo,
53
+ } from './provider';
54
+
55
+ // Convenience hooks (page-level editing)
56
+ export { usePresence, useAeonSync, useAeonData } from './provider';
57
+
58
+ // Navigation hooks (route-level navigation)
59
+ export {
60
+ useAeonNavigation,
61
+ useNavigationPrediction,
62
+ useLinkObserver,
63
+ useTotalPreload,
64
+ useRoutePresence,
65
+ AeonNavigationContext,
66
+ type AeonNavigationContextValue,
67
+ } from './hooks/useAeonNavigation';
68
+
69
+ // Service worker hooks (total preload)
70
+ export {
71
+ useAeonServiceWorker,
72
+ usePreloadProgress,
73
+ useCacheStatus,
74
+ useManualPreload,
75
+ usePrefetchRoute,
76
+ useClearCache,
77
+ type PreloadProgress,
78
+ type CacheStatus,
79
+ } from './hooks/useServiceWorker';
80
+
81
+ // Pilot navigation hooks (AI-driven navigation with consent)
82
+ export {
83
+ usePilotNavigation,
84
+ parseNavigationTags,
85
+ stripNavigationTags,
86
+ type PilotNavigationIntent,
87
+ type PilotNavigationOptions,
88
+ type PilotNavigationState,
89
+ } from './hooks/usePilotNavigation';
@@ -0,0 +1,428 @@
1
+ /**
2
+ * AeonPageProvider - Context provider for Aeon Pages
3
+ *
4
+ * Provides:
5
+ * - Real-time sync via Aeon SyncCoordinator
6
+ * - Presence tracking via AgentPresenceManager
7
+ * - Offline support via OfflineOperationQueue
8
+ * - Schema versioning via SchemaVersionManager
9
+ */
10
+
11
+ import React, { createContext, useContext, useEffect, useState, useCallback, useRef, type ReactNode } from 'react';
12
+
13
+ // Types
14
+ export interface PresenceUser {
15
+ userId: string;
16
+ role: 'user' | 'assistant' | 'monitor' | 'admin';
17
+ cursor?: { x: number; y: number };
18
+ editing?: string;
19
+ status: 'online' | 'away' | 'offline';
20
+ lastActivity: string;
21
+ }
22
+
23
+ export interface SyncState {
24
+ isOnline: boolean;
25
+ isSyncing: boolean;
26
+ lastSyncAt?: string;
27
+ pendingOperations: number;
28
+ }
29
+
30
+ export interface VersionInfo {
31
+ current: string;
32
+ latest: string;
33
+ needsMigration: boolean;
34
+ }
35
+
36
+ export interface AeonPageContextValue {
37
+ // Route info
38
+ route: string;
39
+ sessionId: string;
40
+
41
+ // Presence
42
+ presence: PresenceUser[];
43
+ localUser: PresenceUser | null;
44
+ updateCursor: (position: { x: number; y: number }) => void;
45
+ updateEditing: (elementPath: string | null) => void;
46
+
47
+ // Sync
48
+ sync: SyncState;
49
+ forcSync: () => Promise<void>;
50
+
51
+ // Versioning
52
+ version: VersionInfo;
53
+ migrate: (toVersion: string) => Promise<void>;
54
+
55
+ // Data
56
+ data: Record<string, unknown>;
57
+ setData: (key: string, value: unknown) => void;
58
+
59
+ // Component tree
60
+ tree: unknown;
61
+ updateTree: (path: string, value: unknown) => void;
62
+ }
63
+
64
+ const AeonPageContext = createContext<AeonPageContextValue | null>(null);
65
+
66
+ export interface AeonPageProviderProps {
67
+ route: string;
68
+ children: ReactNode;
69
+ initialData?: Record<string, unknown>;
70
+ }
71
+
72
+ /**
73
+ * AeonPageProvider - Wraps a page with Aeon collaborative features
74
+ */
75
+ export function AeonPageProvider({ route, children, initialData = {} }: AeonPageProviderProps) {
76
+ // Generate session ID from route
77
+ const sessionId = route.replace(/^\/|\/$/g, '').replace(/\//g, '-') || 'index';
78
+
79
+ // State
80
+ const [presence, setPresence] = useState<PresenceUser[]>([]);
81
+ const [localUser, setLocalUser] = useState<PresenceUser | null>(null);
82
+ const [sync, setSync] = useState<SyncState>({
83
+ isOnline: typeof navigator !== 'undefined' ? navigator.onLine : true,
84
+ isSyncing: false,
85
+ pendingOperations: 0,
86
+ });
87
+ const [version, setVersion] = useState<VersionInfo>({
88
+ current: '1.0.0',
89
+ latest: '1.0.0',
90
+ needsMigration: false,
91
+ });
92
+ const [data, setDataState] = useState<Record<string, unknown>>(initialData);
93
+ const [tree, setTree] = useState<unknown>(null);
94
+
95
+ // Refs for Aeon modules (lazy loaded)
96
+ const syncCoordinatorRef = useRef<unknown>(null);
97
+ const presenceManagerRef = useRef<unknown>(null);
98
+ const offlineQueueRef = useRef<unknown>(null);
99
+ const versionManagerRef = useRef<unknown>(null);
100
+ const wsRef = useRef<WebSocket | null>(null);
101
+
102
+ // Initialize Aeon modules
103
+ useEffect(() => {
104
+ const initAeon = async () => {
105
+ try {
106
+ // Dynamic import to avoid SSR issues
107
+ const aeon = await import('@affectively/aeon');
108
+
109
+ // Initialize sync coordinator
110
+ syncCoordinatorRef.current = new aeon.SyncCoordinator();
111
+
112
+ // Initialize presence manager
113
+ presenceManagerRef.current = aeon.AgentPresenceManager.getInstance(sessionId);
114
+
115
+ // Initialize offline queue
116
+ offlineQueueRef.current = aeon.OfflineOperationQueue.getInstance();
117
+
118
+ // Initialize version manager
119
+ versionManagerRef.current = new aeon.SchemaVersionManager();
120
+
121
+ // Set up local user
122
+ const userId = generateUserId();
123
+ setLocalUser({
124
+ userId,
125
+ role: 'user',
126
+ status: 'online',
127
+ lastActivity: new Date().toISOString(),
128
+ });
129
+
130
+ // Connect WebSocket for real-time sync
131
+ connectWebSocket(sessionId);
132
+ } catch (error) {
133
+ console.warn('[aeon-provider] Aeon modules not available:', error);
134
+ }
135
+ };
136
+
137
+ initAeon();
138
+
139
+ // Cleanup
140
+ return () => {
141
+ wsRef.current?.close();
142
+ };
143
+ }, [sessionId]);
144
+
145
+ // Online/offline detection
146
+ useEffect(() => {
147
+ if (typeof window === 'undefined') return;
148
+
149
+ const handleOnline = () => {
150
+ setSync((prev) => ({ ...prev, isOnline: true }));
151
+ // Flush offline queue
152
+ flushOfflineQueue();
153
+ };
154
+
155
+ const handleOffline = () => {
156
+ setSync((prev) => ({ ...prev, isOnline: false }));
157
+ };
158
+
159
+ window.addEventListener('online', handleOnline);
160
+ window.addEventListener('offline', handleOffline);
161
+
162
+ return () => {
163
+ window.removeEventListener('online', handleOnline);
164
+ window.removeEventListener('offline', handleOffline);
165
+ };
166
+ }, []);
167
+
168
+ // Connect WebSocket
169
+ const connectWebSocket = useCallback((sessionId: string) => {
170
+ if (typeof window === 'undefined') return;
171
+
172
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
173
+ const wsUrl = `${protocol}//${window.location.host}/_aeon/ws?session=${sessionId}`;
174
+
175
+ const ws = new WebSocket(wsUrl);
176
+ wsRef.current = ws;
177
+
178
+ ws.onopen = () => {
179
+ console.log('[aeon-provider] WebSocket connected');
180
+ // Join session
181
+ ws.send(JSON.stringify({ type: 'join', sessionId }));
182
+ };
183
+
184
+ ws.onmessage = (event) => {
185
+ try {
186
+ const message = JSON.parse(event.data);
187
+ handleSyncMessage(message);
188
+ } catch (error) {
189
+ console.error('[aeon-provider] Error parsing message:', error);
190
+ }
191
+ };
192
+
193
+ ws.onclose = () => {
194
+ console.log('[aeon-provider] WebSocket disconnected');
195
+ // Reconnect after delay
196
+ setTimeout(() => connectWebSocket(sessionId), 1000);
197
+ };
198
+
199
+ ws.onerror = (error) => {
200
+ console.error('[aeon-provider] WebSocket error:', error);
201
+ };
202
+ }, []);
203
+
204
+ // Handle incoming sync messages
205
+ const handleSyncMessage = useCallback((message: unknown) => {
206
+ const msg = message as { type: string; [key: string]: unknown };
207
+
208
+ switch (msg.type) {
209
+ case 'presence-update':
210
+ setPresence(msg.users as PresenceUser[]);
211
+ break;
212
+
213
+ case 'data-update':
214
+ setDataState((prev) => ({
215
+ ...prev,
216
+ ...(msg.data as Record<string, unknown>),
217
+ }));
218
+ break;
219
+
220
+ case 'tree-update':
221
+ setTree(msg.tree);
222
+ break;
223
+
224
+ case 'version-info':
225
+ setVersion(msg.version as VersionInfo);
226
+ break;
227
+ }
228
+ }, []);
229
+
230
+ // Flush offline queue when back online
231
+ const flushOfflineQueue = useCallback(async () => {
232
+ if (!offlineQueueRef.current) return;
233
+
234
+ setSync((prev) => ({ ...prev, isSyncing: true }));
235
+
236
+ try {
237
+ // @ts-expect-error - Aeon module method
238
+ await offlineQueueRef.current.flush();
239
+ setSync((prev) => ({
240
+ ...prev,
241
+ isSyncing: false,
242
+ pendingOperations: 0,
243
+ lastSyncAt: new Date().toISOString(),
244
+ }));
245
+ } catch (error) {
246
+ console.error('[aeon-provider] Error flushing offline queue:', error);
247
+ setSync((prev) => ({ ...prev, isSyncing: false }));
248
+ }
249
+ }, []);
250
+
251
+ // Update cursor position
252
+ const updateCursor = useCallback((position: { x: number; y: number }) => {
253
+ if (!localUser) return;
254
+
255
+ setLocalUser((prev) =>
256
+ prev ? { ...prev, cursor: position, lastActivity: new Date().toISOString() } : null
257
+ );
258
+
259
+ // Send to WebSocket
260
+ wsRef.current?.send(
261
+ JSON.stringify({
262
+ type: 'cursor-update',
263
+ position,
264
+ })
265
+ );
266
+ }, [localUser]);
267
+
268
+ // Update editing element
269
+ const updateEditing = useCallback((elementPath: string | null) => {
270
+ if (!localUser) return;
271
+
272
+ setLocalUser((prev) =>
273
+ prev ? { ...prev, editing: elementPath ?? undefined, lastActivity: new Date().toISOString() } : null
274
+ );
275
+
276
+ // Send to WebSocket
277
+ wsRef.current?.send(
278
+ JSON.stringify({
279
+ type: 'editing-update',
280
+ elementPath,
281
+ })
282
+ );
283
+ }, [localUser]);
284
+
285
+ // Force sync
286
+ const forceSync = useCallback(async () => {
287
+ if (!sync.isOnline) {
288
+ throw new Error('Cannot sync while offline');
289
+ }
290
+
291
+ setSync((prev) => ({ ...prev, isSyncing: true }));
292
+
293
+ try {
294
+ // @ts-expect-error - Aeon module method
295
+ await syncCoordinatorRef.current?.sync();
296
+ setSync((prev) => ({
297
+ ...prev,
298
+ isSyncing: false,
299
+ lastSyncAt: new Date().toISOString(),
300
+ }));
301
+ } catch (error) {
302
+ setSync((prev) => ({ ...prev, isSyncing: false }));
303
+ throw error;
304
+ }
305
+ }, [sync.isOnline]);
306
+
307
+ // Migrate to new version
308
+ const migrate = useCallback(async (toVersion: string) => {
309
+ // @ts-expect-error - Aeon module method
310
+ await versionManagerRef.current?.migrate(toVersion);
311
+ setVersion((prev) => ({
312
+ ...prev,
313
+ current: toVersion,
314
+ needsMigration: false,
315
+ }));
316
+ }, []);
317
+
318
+ // Set data
319
+ const setData = useCallback((key: string, value: unknown) => {
320
+ setDataState((prev) => ({ ...prev, [key]: value }));
321
+
322
+ // Queue for sync
323
+ if (sync.isOnline && wsRef.current) {
324
+ wsRef.current.send(
325
+ JSON.stringify({
326
+ type: 'data-set',
327
+ key,
328
+ value,
329
+ })
330
+ );
331
+ } else {
332
+ // Queue offline
333
+ // @ts-expect-error - Aeon module method
334
+ offlineQueueRef.current?.enqueue({
335
+ type: 'data-set',
336
+ key,
337
+ value,
338
+ });
339
+ setSync((prev) => ({
340
+ ...prev,
341
+ pendingOperations: prev.pendingOperations + 1,
342
+ }));
343
+ }
344
+ }, [sync.isOnline]);
345
+
346
+ // Update tree
347
+ const updateTree = useCallback((path: string, value: unknown) => {
348
+ // This would apply a patch to the tree
349
+ wsRef.current?.send(
350
+ JSON.stringify({
351
+ type: 'tree-patch',
352
+ path,
353
+ value,
354
+ })
355
+ );
356
+ }, []);
357
+
358
+ // Context value
359
+ const contextValue: AeonPageContextValue = {
360
+ route,
361
+ sessionId,
362
+ presence,
363
+ localUser,
364
+ updateCursor,
365
+ updateEditing,
366
+ sync,
367
+ forcSync: forceSync,
368
+ version,
369
+ migrate,
370
+ data,
371
+ setData,
372
+ tree,
373
+ updateTree,
374
+ };
375
+
376
+ return (
377
+ <AeonPageContext.Provider value={contextValue}>
378
+ {children}
379
+ </AeonPageContext.Provider>
380
+ );
381
+ }
382
+
383
+ /**
384
+ * useAeonPage - Access Aeon page context
385
+ */
386
+ export function useAeonPage(): AeonPageContextValue {
387
+ const context = useContext(AeonPageContext);
388
+ if (!context) {
389
+ throw new Error('useAeonPage must be used within an AeonPageProvider');
390
+ }
391
+ return context;
392
+ }
393
+
394
+ /**
395
+ * usePresence - Just the presence data
396
+ */
397
+ export function usePresence() {
398
+ const { presence, localUser, updateCursor, updateEditing } = useAeonPage();
399
+ return { presence, localUser, updateCursor, updateEditing };
400
+ }
401
+
402
+ /**
403
+ * useAeonSync - Just the sync state
404
+ */
405
+ export function useAeonSync() {
406
+ const { sync, forcSync: forceSync } = useAeonPage();
407
+ return { ...sync, forceSync };
408
+ }
409
+
410
+ /**
411
+ * useAeonData - Just the data store
412
+ */
413
+ export function useAeonData<T = unknown>(key: string): [T | undefined, (value: T) => void] {
414
+ const { data, setData } = useAeonPage();
415
+ const value = data[key] as T | undefined;
416
+ const setValue = useCallback((newValue: T) => setData(key, newValue), [key, setData]);
417
+ return [value, setValue];
418
+ }
419
+
420
+ // Helper to generate user ID
421
+ function generateUserId(): string {
422
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
423
+ return crypto.randomUUID();
424
+ }
425
+ return `user-${Date.now()}-${Math.random().toString(36).slice(2)}`;
426
+ }
427
+
428
+ export default AeonPageProvider;
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@affectively/aeon-pages-runtime",
3
+ "version": "0.2.0",
4
+ "description": "Bun/Cloudflare runtime for @affectively/aeon-pages",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./server": {
14
+ "import": "./dist/server.js",
15
+ "types": "./dist/server.d.ts"
16
+ },
17
+ "./router": {
18
+ "import": "./dist/router/index.js",
19
+ "types": "./dist/router/index.d.ts"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "build": "bun build ./src/index.ts ./src/server.ts ./src/router/index.ts --outdir ./dist --target bun && tsc --declaration --emitDeclarationOnly",
24
+ "dev": "bun --watch ./src/index.ts",
25
+ "test": "bun test",
26
+ "prepublishOnly": "bun run build",
27
+ "deploy": "wrangler deploy",
28
+ "deploy:staging": "wrangler deploy --env staging",
29
+ "dev:worker": "wrangler dev",
30
+ "db:create": "wrangler d1 create aeon-pages",
31
+ "db:migrate": "wrangler d1 execute aeon-pages --file=./schema.sql",
32
+ "db:migrate:local": "wrangler d1 execute aeon-pages --local --file=./schema.sql",
33
+ "tail": "wrangler tail"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md"
38
+ ],
39
+ "peerDependencies": {
40
+ "bun": ">=1.0.0",
41
+ "react": ">=18.0.0",
42
+ "zod": ">=3.0.0",
43
+ "@affectively/aeon": ">=0.1.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "@affectively/aeon": {
47
+ "optional": true
48
+ }
49
+ },
50
+ "devDependencies": {
51
+ "typescript": "^5.7.0",
52
+ "@types/bun": "latest",
53
+ "@types/react": "^19.0.0",
54
+ "react": "^19.0.0",
55
+ "zod": "^3.24.0"
56
+ },
57
+ "license": "MIT",
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/affectively/aeon-pages"
61
+ },
62
+ "keywords": [
63
+ "aeon",
64
+ "pages",
65
+ "framework",
66
+ "collaborative",
67
+ "bun",
68
+ "cloudflare"
69
+ ]
70
+ }
@@ -0,0 +1,40 @@
1
+ -- Aeon Pages D1 Schema
2
+ -- Run with: wrangler d1 execute aeon-pages --file=./schema.sql
3
+
4
+ -- Sessions table (async propagation from Durable Objects)
5
+ CREATE TABLE IF NOT EXISTS sessions (
6
+ session_id TEXT PRIMARY KEY,
7
+ route TEXT NOT NULL,
8
+ tree TEXT NOT NULL, -- JSON serialized component tree
9
+ data TEXT, -- JSON serialized session data
10
+ schema_version TEXT NOT NULL,
11
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
12
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
13
+ );
14
+
15
+ -- Routes table (backup for route registry)
16
+ CREATE TABLE IF NOT EXISTS routes (
17
+ pattern TEXT PRIMARY KEY,
18
+ session_id TEXT,
19
+ component_id TEXT,
20
+ is_aeon BOOLEAN DEFAULT FALSE,
21
+ metadata TEXT, -- JSON
22
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
23
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
24
+ );
25
+
26
+ -- Presence snapshots (for analytics)
27
+ CREATE TABLE IF NOT EXISTS presence_snapshots (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ session_id TEXT NOT NULL,
30
+ user_count INTEGER NOT NULL,
31
+ users TEXT, -- JSON array of user IDs
32
+ snapshot_at DATETIME DEFAULT CURRENT_TIMESTAMP,
33
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
34
+ );
35
+
36
+ -- Indexes
37
+ CREATE INDEX IF NOT EXISTS idx_sessions_route ON sessions(route);
38
+ CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at);
39
+ CREATE INDEX IF NOT EXISTS idx_presence_session ON presence_snapshots(session_id);
40
+ CREATE INDEX IF NOT EXISTS idx_presence_time ON presence_snapshots(snapshot_at);