@affectively/aeon-pages 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,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
+ }