@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.
- package/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -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 +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -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 +192 -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 +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
|
@@ -0,0 +1,151 @@
|
|
|
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 {
|
|
43
|
+
Link,
|
|
44
|
+
type LinkProps,
|
|
45
|
+
type TransitionType,
|
|
46
|
+
type PrefetchStrategy,
|
|
47
|
+
type PresenceRenderProps,
|
|
48
|
+
} from './Link';
|
|
49
|
+
|
|
50
|
+
// Provider and main hook
|
|
51
|
+
export {
|
|
52
|
+
AeonPageProvider,
|
|
53
|
+
useAeonPage,
|
|
54
|
+
type AeonPageProviderProps,
|
|
55
|
+
type AeonPageContextValue,
|
|
56
|
+
type PresenceUser,
|
|
57
|
+
type SyncState,
|
|
58
|
+
type VersionInfo,
|
|
59
|
+
} from './provider';
|
|
60
|
+
|
|
61
|
+
// Convenience hooks (page-level editing)
|
|
62
|
+
export { usePresence, useAeonSync, useAeonData } from './provider';
|
|
63
|
+
|
|
64
|
+
// Navigation hooks (route-level navigation)
|
|
65
|
+
export {
|
|
66
|
+
useAeonNavigation,
|
|
67
|
+
useNavigationPrediction,
|
|
68
|
+
useLinkObserver,
|
|
69
|
+
useTotalPreload,
|
|
70
|
+
useRoutePresence,
|
|
71
|
+
AeonNavigationContext,
|
|
72
|
+
type AeonNavigationContextValue,
|
|
73
|
+
} from './hooks/useAeonNavigation';
|
|
74
|
+
|
|
75
|
+
// Service worker hooks (total preload)
|
|
76
|
+
export {
|
|
77
|
+
useAeonServiceWorker,
|
|
78
|
+
usePreloadProgress,
|
|
79
|
+
useCacheStatus,
|
|
80
|
+
useManualPreload,
|
|
81
|
+
usePrefetchRoute,
|
|
82
|
+
useClearCache,
|
|
83
|
+
type PreloadProgress,
|
|
84
|
+
type CacheStatus,
|
|
85
|
+
} from './hooks/useServiceWorker';
|
|
86
|
+
|
|
87
|
+
// Pilot navigation hooks (AI-driven navigation with consent)
|
|
88
|
+
export {
|
|
89
|
+
usePilotNavigation,
|
|
90
|
+
parseNavigationTags,
|
|
91
|
+
stripNavigationTags,
|
|
92
|
+
type PilotNavigationIntent,
|
|
93
|
+
type PilotNavigationOptions,
|
|
94
|
+
type PilotNavigationState,
|
|
95
|
+
} from './hooks/usePilotNavigation';
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Offline Luxury Features
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
// Network state hook
|
|
102
|
+
export {
|
|
103
|
+
useNetworkState,
|
|
104
|
+
type NetworkState,
|
|
105
|
+
type BandwidthProfile,
|
|
106
|
+
type NetworkStateResult,
|
|
107
|
+
} from './hooks/useNetworkState';
|
|
108
|
+
|
|
109
|
+
// Conflict management hook
|
|
110
|
+
export {
|
|
111
|
+
useConflicts,
|
|
112
|
+
addConflict,
|
|
113
|
+
getAllConflicts,
|
|
114
|
+
clearAllConflicts,
|
|
115
|
+
type Conflict,
|
|
116
|
+
type ConflictStats,
|
|
117
|
+
type ResolutionStrategy,
|
|
118
|
+
type UseConflictsResult,
|
|
119
|
+
} from './hooks/useConflicts';
|
|
120
|
+
|
|
121
|
+
// PWA install prompt component
|
|
122
|
+
export {
|
|
123
|
+
InstallPrompt,
|
|
124
|
+
useInstallPrompt,
|
|
125
|
+
type InstallPromptProps,
|
|
126
|
+
type InstallPromptState,
|
|
127
|
+
} from './components/InstallPrompt';
|
|
128
|
+
|
|
129
|
+
// Push notifications component
|
|
130
|
+
export {
|
|
131
|
+
PushNotifications,
|
|
132
|
+
usePushNotifications,
|
|
133
|
+
type PushNotificationsProps,
|
|
134
|
+
type PushSubscriptionData,
|
|
135
|
+
type PushNotificationState,
|
|
136
|
+
type UsePushNotificationsConfig,
|
|
137
|
+
} from './components/PushNotifications';
|
|
138
|
+
|
|
139
|
+
// Offline diagnostics component
|
|
140
|
+
export {
|
|
141
|
+
OfflineDiagnostics,
|
|
142
|
+
NetworkStatusPanel,
|
|
143
|
+
ServiceWorkerPanel,
|
|
144
|
+
CacheManagementPanel,
|
|
145
|
+
QueueStatsPanel,
|
|
146
|
+
ConflictsPanel,
|
|
147
|
+
type OfflineDiagnosticsProps,
|
|
148
|
+
type ServiceWorkerState,
|
|
149
|
+
type CacheInfo,
|
|
150
|
+
type QueueStats,
|
|
151
|
+
} from './components/OfflineDiagnostics';
|
|
@@ -0,0 +1,467 @@
|
|
|
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, {
|
|
12
|
+
createContext,
|
|
13
|
+
useContext,
|
|
14
|
+
useEffect,
|
|
15
|
+
useState,
|
|
16
|
+
useCallback,
|
|
17
|
+
useRef,
|
|
18
|
+
type ReactNode,
|
|
19
|
+
} from 'react';
|
|
20
|
+
import { getSyncCoordinator } from '@affectively/aeon-pages-runtime/sync/coordinator';
|
|
21
|
+
import { getOfflineQueue } from '@affectively/aeon-pages-runtime/offline/encrypted-queue';
|
|
22
|
+
|
|
23
|
+
// Types
|
|
24
|
+
export interface PresenceUser {
|
|
25
|
+
userId: string;
|
|
26
|
+
role: 'user' | 'assistant' | 'monitor' | 'admin';
|
|
27
|
+
cursor?: { x: number; y: number };
|
|
28
|
+
editing?: string;
|
|
29
|
+
status: 'online' | 'away' | 'offline';
|
|
30
|
+
lastActivity: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SyncState {
|
|
34
|
+
isOnline: boolean;
|
|
35
|
+
isSyncing: boolean;
|
|
36
|
+
lastSyncAt?: string;
|
|
37
|
+
pendingOperations: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface VersionInfo {
|
|
41
|
+
current: string;
|
|
42
|
+
latest: string;
|
|
43
|
+
needsMigration: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AeonPageContextValue {
|
|
47
|
+
// Route info
|
|
48
|
+
route: string;
|
|
49
|
+
sessionId: string;
|
|
50
|
+
|
|
51
|
+
// Presence
|
|
52
|
+
presence: PresenceUser[];
|
|
53
|
+
localUser: PresenceUser | null;
|
|
54
|
+
updateCursor: (position: { x: number; y: number }) => void;
|
|
55
|
+
updateEditing: (elementPath: string | null) => void;
|
|
56
|
+
|
|
57
|
+
// Sync
|
|
58
|
+
sync: SyncState;
|
|
59
|
+
forcSync: () => Promise<void>;
|
|
60
|
+
|
|
61
|
+
// Versioning
|
|
62
|
+
version: VersionInfo;
|
|
63
|
+
migrate: (toVersion: string) => Promise<void>;
|
|
64
|
+
|
|
65
|
+
// Data
|
|
66
|
+
data: Record<string, unknown>;
|
|
67
|
+
setData: (key: string, value: unknown) => void;
|
|
68
|
+
|
|
69
|
+
// Component tree
|
|
70
|
+
tree: unknown;
|
|
71
|
+
updateTree: (path: string, value: unknown) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const AeonPageContext = createContext<AeonPageContextValue | null>(null);
|
|
75
|
+
|
|
76
|
+
export interface AeonPageProviderProps {
|
|
77
|
+
route: string;
|
|
78
|
+
children: ReactNode;
|
|
79
|
+
initialData?: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* AeonPageProvider - Wraps a page with Aeon collaborative features
|
|
84
|
+
*/
|
|
85
|
+
export function AeonPageProvider({
|
|
86
|
+
route,
|
|
87
|
+
children,
|
|
88
|
+
initialData = {},
|
|
89
|
+
}: AeonPageProviderProps) {
|
|
90
|
+
// Generate session ID from route
|
|
91
|
+
const sessionId =
|
|
92
|
+
route.replace(/^\/|\/$/g, '').replace(/\//g, '-') || 'index';
|
|
93
|
+
|
|
94
|
+
// State
|
|
95
|
+
const [presence, setPresence] = useState<PresenceUser[]>([]);
|
|
96
|
+
const [localUser, setLocalUser] = useState<PresenceUser | null>(null);
|
|
97
|
+
const [sync, setSync] = useState<SyncState>({
|
|
98
|
+
isOnline: typeof navigator !== 'undefined' ? navigator.onLine : true,
|
|
99
|
+
isSyncing: false,
|
|
100
|
+
pendingOperations: 0,
|
|
101
|
+
});
|
|
102
|
+
const [version, setVersion] = useState<VersionInfo>({
|
|
103
|
+
current: '1.0.0',
|
|
104
|
+
latest: '1.0.0',
|
|
105
|
+
needsMigration: false,
|
|
106
|
+
});
|
|
107
|
+
const [data, setDataState] = useState<Record<string, unknown>>(initialData);
|
|
108
|
+
const [tree, setTree] = useState<unknown>(null);
|
|
109
|
+
|
|
110
|
+
// Refs for Aeon modules (lazy loaded)
|
|
111
|
+
const syncCoordinatorRef = useRef<unknown>(null);
|
|
112
|
+
const presenceManagerRef = useRef<unknown>(null);
|
|
113
|
+
const offlineQueueRef = useRef<unknown>(null);
|
|
114
|
+
const versionManagerRef = useRef<unknown>(null);
|
|
115
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
116
|
+
|
|
117
|
+
// Initialize Aeon modules
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const initAeon = async () => {
|
|
120
|
+
try {
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
// Initialize sync coordinator
|
|
125
|
+
syncCoordinatorRef.current = getSyncCoordinator();
|
|
126
|
+
|
|
127
|
+
// Initialize offline queue
|
|
128
|
+
offlineQueueRef.current = getOfflineQueue();
|
|
129
|
+
|
|
130
|
+
// Presence manager and version manager are handled via WebSocket
|
|
131
|
+
presenceManagerRef.current = null;
|
|
132
|
+
versionManagerRef.current = null;
|
|
133
|
+
|
|
134
|
+
// Set up local user
|
|
135
|
+
const userId = generateUserId();
|
|
136
|
+
setLocalUser({
|
|
137
|
+
userId,
|
|
138
|
+
role: 'user',
|
|
139
|
+
status: 'online',
|
|
140
|
+
lastActivity: new Date().toISOString(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Connect WebSocket for real-time sync
|
|
144
|
+
connectWebSocket(sessionId);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.warn('[aeon-provider] Aeon modules not available:', error);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
initAeon();
|
|
151
|
+
|
|
152
|
+
// Cleanup
|
|
153
|
+
return () => {
|
|
154
|
+
wsRef.current?.close();
|
|
155
|
+
};
|
|
156
|
+
}, [sessionId]);
|
|
157
|
+
|
|
158
|
+
// Online/offline detection
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (typeof window === 'undefined') return;
|
|
161
|
+
|
|
162
|
+
const handleOnline = () => {
|
|
163
|
+
setSync((prev) => ({ ...prev, isOnline: true }));
|
|
164
|
+
// Flush offline queue
|
|
165
|
+
flushOfflineQueue();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handleOffline = () => {
|
|
169
|
+
setSync((prev) => ({ ...prev, isOnline: false }));
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
window.addEventListener('online', handleOnline);
|
|
173
|
+
window.addEventListener('offline', handleOffline);
|
|
174
|
+
|
|
175
|
+
return () => {
|
|
176
|
+
window.removeEventListener('online', handleOnline);
|
|
177
|
+
window.removeEventListener('offline', handleOffline);
|
|
178
|
+
};
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
181
|
+
// Connect WebSocket
|
|
182
|
+
const connectWebSocket = useCallback((sessionId: string) => {
|
|
183
|
+
if (typeof window === 'undefined') return;
|
|
184
|
+
|
|
185
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
186
|
+
const wsUrl = `${protocol}//${window.location.host}/_aeon/ws?session=${sessionId}`;
|
|
187
|
+
|
|
188
|
+
const ws = new WebSocket(wsUrl);
|
|
189
|
+
wsRef.current = ws;
|
|
190
|
+
|
|
191
|
+
ws.onopen = () => {
|
|
192
|
+
console.log('[aeon-provider] WebSocket connected');
|
|
193
|
+
// Join session
|
|
194
|
+
ws.send(JSON.stringify({ type: 'join', sessionId }));
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
ws.onmessage = (event) => {
|
|
198
|
+
try {
|
|
199
|
+
const message = JSON.parse(event.data);
|
|
200
|
+
handleSyncMessage(message);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error('[aeon-provider] Error parsing message:', error);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
ws.onclose = () => {
|
|
207
|
+
console.log('[aeon-provider] WebSocket disconnected');
|
|
208
|
+
// Reconnect after delay
|
|
209
|
+
setTimeout(() => connectWebSocket(sessionId), 1000);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
ws.onerror = (error) => {
|
|
213
|
+
console.error('[aeon-provider] WebSocket error:', error);
|
|
214
|
+
};
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
// Handle incoming sync messages
|
|
218
|
+
const handleSyncMessage = useCallback((message: unknown) => {
|
|
219
|
+
const msg = message as { type: string; [key: string]: unknown };
|
|
220
|
+
|
|
221
|
+
switch (msg.type) {
|
|
222
|
+
case 'presence-update':
|
|
223
|
+
setPresence(msg.users as PresenceUser[]);
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case 'data-update':
|
|
227
|
+
setDataState((prev) => ({
|
|
228
|
+
...prev,
|
|
229
|
+
...(msg.data as Record<string, unknown>),
|
|
230
|
+
}));
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case 'tree-update':
|
|
234
|
+
setTree(msg.tree);
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case 'version-info':
|
|
238
|
+
setVersion(msg.version as VersionInfo);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}, []);
|
|
242
|
+
|
|
243
|
+
// Flush offline queue when back online
|
|
244
|
+
const flushOfflineQueue = useCallback(async () => {
|
|
245
|
+
if (!offlineQueueRef.current) return;
|
|
246
|
+
|
|
247
|
+
setSync((prev) => ({ ...prev, isSyncing: true }));
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// @ts-expect-error - Aeon module method
|
|
251
|
+
await offlineQueueRef.current.flush();
|
|
252
|
+
setSync((prev) => ({
|
|
253
|
+
...prev,
|
|
254
|
+
isSyncing: false,
|
|
255
|
+
pendingOperations: 0,
|
|
256
|
+
lastSyncAt: new Date().toISOString(),
|
|
257
|
+
}));
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('[aeon-provider] Error flushing offline queue:', error);
|
|
260
|
+
setSync((prev) => ({ ...prev, isSyncing: false }));
|
|
261
|
+
}
|
|
262
|
+
}, []);
|
|
263
|
+
|
|
264
|
+
// Update cursor position
|
|
265
|
+
const updateCursor = useCallback(
|
|
266
|
+
(position: { x: number; y: number }) => {
|
|
267
|
+
if (!localUser) return;
|
|
268
|
+
|
|
269
|
+
setLocalUser((prev) =>
|
|
270
|
+
prev
|
|
271
|
+
? {
|
|
272
|
+
...prev,
|
|
273
|
+
cursor: position,
|
|
274
|
+
lastActivity: new Date().toISOString(),
|
|
275
|
+
}
|
|
276
|
+
: null,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Send to WebSocket
|
|
280
|
+
wsRef.current?.send(
|
|
281
|
+
JSON.stringify({
|
|
282
|
+
type: 'cursor-update',
|
|
283
|
+
position,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
},
|
|
287
|
+
[localUser],
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Update editing element
|
|
291
|
+
const updateEditing = useCallback(
|
|
292
|
+
(elementPath: string | null) => {
|
|
293
|
+
if (!localUser) return;
|
|
294
|
+
|
|
295
|
+
setLocalUser((prev) =>
|
|
296
|
+
prev
|
|
297
|
+
? {
|
|
298
|
+
...prev,
|
|
299
|
+
editing: elementPath ?? undefined,
|
|
300
|
+
lastActivity: new Date().toISOString(),
|
|
301
|
+
}
|
|
302
|
+
: null,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Send to WebSocket
|
|
306
|
+
wsRef.current?.send(
|
|
307
|
+
JSON.stringify({
|
|
308
|
+
type: 'editing-update',
|
|
309
|
+
elementPath,
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
},
|
|
313
|
+
[localUser],
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Force sync
|
|
317
|
+
const forceSync = useCallback(async () => {
|
|
318
|
+
if (!sync.isOnline) {
|
|
319
|
+
throw new Error('Cannot sync while offline');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
setSync((prev) => ({ ...prev, isSyncing: true }));
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
// @ts-expect-error - Aeon module method
|
|
326
|
+
await syncCoordinatorRef.current?.sync();
|
|
327
|
+
setSync((prev) => ({
|
|
328
|
+
...prev,
|
|
329
|
+
isSyncing: false,
|
|
330
|
+
lastSyncAt: new Date().toISOString(),
|
|
331
|
+
}));
|
|
332
|
+
} catch (error) {
|
|
333
|
+
setSync((prev) => ({ ...prev, isSyncing: false }));
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}, [sync.isOnline]);
|
|
337
|
+
|
|
338
|
+
// Migrate to new version
|
|
339
|
+
const migrate = useCallback(async (toVersion: string) => {
|
|
340
|
+
// @ts-expect-error - Aeon module method
|
|
341
|
+
await versionManagerRef.current?.migrate(toVersion);
|
|
342
|
+
setVersion((prev) => ({
|
|
343
|
+
...prev,
|
|
344
|
+
current: toVersion,
|
|
345
|
+
needsMigration: false,
|
|
346
|
+
}));
|
|
347
|
+
}, []);
|
|
348
|
+
|
|
349
|
+
// Set data
|
|
350
|
+
const setData = useCallback(
|
|
351
|
+
(key: string, value: unknown) => {
|
|
352
|
+
setDataState((prev) => ({ ...prev, [key]: value }));
|
|
353
|
+
|
|
354
|
+
// Queue for sync
|
|
355
|
+
if (sync.isOnline && wsRef.current) {
|
|
356
|
+
wsRef.current.send(
|
|
357
|
+
JSON.stringify({
|
|
358
|
+
type: 'data-set',
|
|
359
|
+
key,
|
|
360
|
+
value,
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
} else {
|
|
364
|
+
// Queue offline
|
|
365
|
+
// @ts-expect-error - Aeon module method
|
|
366
|
+
offlineQueueRef.current?.enqueue({
|
|
367
|
+
type: 'data-set',
|
|
368
|
+
key,
|
|
369
|
+
value,
|
|
370
|
+
});
|
|
371
|
+
setSync((prev) => ({
|
|
372
|
+
...prev,
|
|
373
|
+
pendingOperations: prev.pendingOperations + 1,
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
[sync.isOnline],
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Update tree
|
|
381
|
+
const updateTree = useCallback((path: string, value: unknown) => {
|
|
382
|
+
// This would apply a patch to the tree
|
|
383
|
+
wsRef.current?.send(
|
|
384
|
+
JSON.stringify({
|
|
385
|
+
type: 'tree-patch',
|
|
386
|
+
path,
|
|
387
|
+
value,
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
}, []);
|
|
391
|
+
|
|
392
|
+
// Context value
|
|
393
|
+
const contextValue: AeonPageContextValue = {
|
|
394
|
+
route,
|
|
395
|
+
sessionId,
|
|
396
|
+
presence,
|
|
397
|
+
localUser,
|
|
398
|
+
updateCursor,
|
|
399
|
+
updateEditing,
|
|
400
|
+
sync,
|
|
401
|
+
forcSync: forceSync,
|
|
402
|
+
version,
|
|
403
|
+
migrate,
|
|
404
|
+
data,
|
|
405
|
+
setData,
|
|
406
|
+
tree,
|
|
407
|
+
updateTree,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<AeonPageContext.Provider value={contextValue}>
|
|
412
|
+
{children}
|
|
413
|
+
</AeonPageContext.Provider>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* useAeonPage - Access Aeon page context
|
|
419
|
+
*/
|
|
420
|
+
export function useAeonPage(): AeonPageContextValue {
|
|
421
|
+
const context = useContext(AeonPageContext);
|
|
422
|
+
if (!context) {
|
|
423
|
+
throw new Error('useAeonPage must be used within an AeonPageProvider');
|
|
424
|
+
}
|
|
425
|
+
return context;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* usePresence - Just the presence data
|
|
430
|
+
*/
|
|
431
|
+
export function usePresence() {
|
|
432
|
+
const { presence, localUser, updateCursor, updateEditing } = useAeonPage();
|
|
433
|
+
return { presence, localUser, updateCursor, updateEditing };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* useAeonSync - Just the sync state
|
|
438
|
+
*/
|
|
439
|
+
export function useAeonSync() {
|
|
440
|
+
const { sync, forcSync: forceSync } = useAeonPage();
|
|
441
|
+
return { ...sync, forceSync };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* useAeonData - Just the data store
|
|
446
|
+
*/
|
|
447
|
+
export function useAeonData<T = unknown>(
|
|
448
|
+
key: string,
|
|
449
|
+
): [T | undefined, (value: T) => void] {
|
|
450
|
+
const { data, setData } = useAeonPage();
|
|
451
|
+
const value = data[key] as T | undefined;
|
|
452
|
+
const setValue = useCallback(
|
|
453
|
+
(newValue: T) => setData(key, newValue),
|
|
454
|
+
[key, setData],
|
|
455
|
+
);
|
|
456
|
+
return [value, setValue];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Helper to generate user ID
|
|
460
|
+
function generateUserId(): string {
|
|
461
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
462
|
+
return crypto.randomUUID();
|
|
463
|
+
}
|
|
464
|
+
return `user-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export default AeonPageProvider;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ESNext", "DOM"],
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationDir": "./dist",
|
|
9
|
+
"emitDeclarationOnly": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src"
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
19
|
+
}
|