@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
|
@@ -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);
|