@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,230 @@
1
+ /**
2
+ * @affectively/aeon-pages-analytics
3
+ *
4
+ * Automatic click tracking and GTM integration for aeon-pages.
5
+ *
6
+ * Features:
7
+ * - Merkle tree-based node identification
8
+ * - Zero-instrumentation click tracking
9
+ * - ESI context → GTM dataLayer sync
10
+ * - Full tree path with every click event
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * import { AeonAnalyticsProvider } from '@affectively/aeon-pages-analytics/react';
15
+ *
16
+ * export default function App({ Component, pageProps }) {
17
+ * return (
18
+ * <AeonAnalyticsProvider
19
+ * gtmContainerId="GTM-XXXXXX"
20
+ * trackClicks={true}
21
+ * syncESIContext={true}
22
+ * >
23
+ * <Component {...pageProps} />
24
+ * </AeonAnalyticsProvider>
25
+ * );
26
+ * }
27
+ * ```
28
+ */
29
+
30
+ // ============================================================================
31
+ // Types
32
+ // ============================================================================
33
+
34
+ export type {
35
+ // Configuration
36
+ GTMConfig,
37
+ AnalyticsConfig,
38
+ ClickTrackingOptions,
39
+
40
+ // Merkle Tree
41
+ MerkleNode,
42
+ MerkleTree,
43
+ ComponentNode,
44
+ ComponentTree,
45
+ SerializedMerkleInfo,
46
+
47
+ // ESI State
48
+ UserTier,
49
+ ConnectionType,
50
+ EmotionState,
51
+ ESIState,
52
+ ESIStateFeatures,
53
+
54
+ // DataLayer Events
55
+ AeonEventBase,
56
+ ElementInfo,
57
+ PositionInfo,
58
+ ContextEvent,
59
+ PageViewEvent,
60
+ ClickEvent,
61
+ DataLayerEvent,
62
+ } from './types';
63
+
64
+ // Constants
65
+ export { MERKLE_ATTR, PATH_ATTR, PATH_HASHES_ATTR, TYPE_ATTR } from './types';
66
+
67
+ // ============================================================================
68
+ // Merkle Tree
69
+ // ============================================================================
70
+
71
+ export {
72
+ // Async builders
73
+ hashNodeAsync,
74
+ buildMerkleTree,
75
+
76
+ // Sync builders
77
+ hashNodeSync,
78
+ buildMerkleTreeSync,
79
+
80
+ // DOM helpers
81
+ getMerkleAttributes,
82
+ parseMerkleFromElement,
83
+ findNearestMerkleElement,
84
+
85
+ // Verification
86
+ verifyMerkleTree,
87
+ diffMerkleTrees,
88
+ } from './merkle-tree';
89
+
90
+ // ============================================================================
91
+ // DataLayer
92
+ // ============================================================================
93
+
94
+ export {
95
+ // Version
96
+ ANALYTICS_VERSION,
97
+
98
+ // DataLayer management
99
+ ensureDataLayer,
100
+ pushToDataLayer,
101
+ getDataLayer,
102
+ clearDataLayer,
103
+
104
+ // Event builders
105
+ buildContextEvent,
106
+ buildPageViewEvent,
107
+ buildClickEvent,
108
+
109
+ // Element/position extraction
110
+ extractElementInfo,
111
+ extractPositionInfo,
112
+
113
+ // Push helpers
114
+ pushContextEvent,
115
+ pushPageViewEvent,
116
+ pushClickEvent,
117
+
118
+ // Debug
119
+ setDebugMode,
120
+ } from './data-layer';
121
+
122
+ // ============================================================================
123
+ // GTM Loader
124
+ // ============================================================================
125
+
126
+ export {
127
+ // Injection
128
+ injectGTM,
129
+ injectGTMNoScript,
130
+ initializeGTM,
131
+
132
+ // SSR helpers
133
+ generateGTMScriptTag,
134
+ generateGTMNoScriptTag,
135
+ generateDataLayerScript,
136
+
137
+ // Status
138
+ isGTMInjected,
139
+ isGTMReady,
140
+ waitForGTM,
141
+
142
+ // Testing
143
+ resetGTMState,
144
+ } from './gtm-loader';
145
+
146
+ // ============================================================================
147
+ // Context Bridge
148
+ // ============================================================================
149
+
150
+ export {
151
+ // ESI state access
152
+ getESIState,
153
+ hasESIState,
154
+ getESIProperty,
155
+
156
+ // Subscription
157
+ subscribeToESIChanges,
158
+
159
+ // DataLayer sync
160
+ syncESIToDataLayer,
161
+ pushPageView,
162
+
163
+ // Watch mode
164
+ watchESIChanges,
165
+ initContextBridge,
166
+
167
+ // Retry logic
168
+ waitForESIState,
169
+ initContextBridgeWithRetry,
170
+
171
+ // Utilities
172
+ getESIContextSnapshot,
173
+ isAdmin,
174
+ hasFeature,
175
+ meetsTierRequirement,
176
+ getUserTier,
177
+ getEmotionState,
178
+ } from './context-bridge';
179
+
180
+ // ============================================================================
181
+ // Click Tracker
182
+ // ============================================================================
183
+
184
+ export {
185
+ // Initialization
186
+ initClickTracker,
187
+ stopClickTracker,
188
+ isClickTrackerActive,
189
+
190
+ // Manual tracking
191
+ trackClick,
192
+ trackInteraction,
193
+ } from './click-tracker';
194
+
195
+ // ============================================================================
196
+ // React
197
+ // ============================================================================
198
+
199
+ export {
200
+ // Main hook
201
+ useAeonAnalytics,
202
+ type UseAnalyticsReturn,
203
+
204
+ // Utility hooks
205
+ useESIState,
206
+ useTrackMount,
207
+ useTrackVisibility,
208
+ } from './use-analytics';
209
+
210
+ export {
211
+ // Provider
212
+ AeonAnalyticsProvider,
213
+ type AeonAnalyticsProviderProps,
214
+
215
+ // Context hooks
216
+ useAnalytics,
217
+ useAnalyticsOptional,
218
+
219
+ // HOC
220
+ withAnalytics,
221
+
222
+ // Render props
223
+ Analytics,
224
+
225
+ // Merkle tree provider
226
+ MerkleTreeProvider,
227
+
228
+ // Track component
229
+ Track,
230
+ } from './provider';
@@ -0,0 +1,489 @@
1
+ /**
2
+ * Merkle Tree Node Identification
3
+ *
4
+ * Generates deterministic, content-addressable hashes for component tree nodes.
5
+ * Uses SHA-256 with children hashes to create a Merkle tree structure.
6
+ *
7
+ * Benefits:
8
+ * - Stable IDs across renders (if content unchanged)
9
+ * - Content-addressable (same content = same hash)
10
+ * - Tree-aware (parent hash changes if any child changes)
11
+ * - Position-independent (moves don't change hash)
12
+ */
13
+
14
+ import type {
15
+ ComponentNode,
16
+ ComponentTree,
17
+ MerkleNode,
18
+ MerkleTree,
19
+ MERKLE_ATTR,
20
+ PATH_ATTR,
21
+ PATH_HASHES_ATTR,
22
+ TYPE_ATTR,
23
+ } from './types';
24
+
25
+ // ============================================================================
26
+ // Crypto Utilities
27
+ // ============================================================================
28
+
29
+ /**
30
+ * SHA-256 hash with Web Crypto API (works in browser and Bun/Node)
31
+ */
32
+ async function sha256(message: string): Promise<string> {
33
+ const encoder = new TextEncoder();
34
+ const data = encoder.encode(message);
35
+
36
+ // Use Web Crypto API
37
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
38
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
39
+ const hashHex = hashArray
40
+ .map((b) => b.toString(16).padStart(2, '0'))
41
+ .join('');
42
+
43
+ return hashHex;
44
+ }
45
+
46
+ /**
47
+ * Synchronous hash using simple djb2 algorithm for performance
48
+ * Used for quick hashing when async is not desired
49
+ */
50
+ function djb2Hash(str: string): string {
51
+ let hash = 5381;
52
+ for (let i = 0; i < str.length; i++) {
53
+ hash = (hash * 33) ^ str.charCodeAt(i);
54
+ }
55
+ // Convert to positive hex, pad to 12 chars
56
+ return ((hash >>> 0).toString(16) + (hash >>> 0).toString(16)).slice(0, 12);
57
+ }
58
+
59
+ /**
60
+ * Truncate hash to 12 characters for compact storage
61
+ */
62
+ function truncateHash(hash: string): string {
63
+ return hash.slice(0, 12);
64
+ }
65
+
66
+ // ============================================================================
67
+ // Key Sorting for Deterministic JSON
68
+ // ============================================================================
69
+
70
+ /**
71
+ * Sort object keys recursively for deterministic serialization
72
+ */
73
+ function sortKeys(obj: unknown): unknown {
74
+ if (obj === null || typeof obj !== 'object') {
75
+ return obj;
76
+ }
77
+
78
+ if (Array.isArray(obj)) {
79
+ return obj.map(sortKeys);
80
+ }
81
+
82
+ const sorted: Record<string, unknown> = {};
83
+ const keys = Object.keys(obj as Record<string, unknown>).sort();
84
+
85
+ for (const key of keys) {
86
+ sorted[key] = sortKeys((obj as Record<string, unknown>)[key]);
87
+ }
88
+
89
+ return sorted;
90
+ }
91
+
92
+ /**
93
+ * Sanitize props for hashing - remove functions, symbols, etc.
94
+ */
95
+ function sanitizeProps(
96
+ props: Record<string, unknown> | undefined,
97
+ ): Record<string, unknown> {
98
+ if (!props) return {};
99
+
100
+ const sanitized: Record<string, unknown> = {};
101
+
102
+ for (const [key, value] of Object.entries(props)) {
103
+ // Skip functions, symbols, and React internals
104
+ if (
105
+ typeof value === 'function' ||
106
+ typeof value === 'symbol' ||
107
+ key.startsWith('_') ||
108
+ key === 'children' ||
109
+ key === 'ref' ||
110
+ key === 'key'
111
+ ) {
112
+ continue;
113
+ }
114
+
115
+ // Recursively sanitize nested objects
116
+ if (value !== null && typeof value === 'object') {
117
+ sanitized[key] = sanitizeProps(value as Record<string, unknown>);
118
+ } else {
119
+ sanitized[key] = value;
120
+ }
121
+ }
122
+
123
+ return sanitized;
124
+ }
125
+
126
+ // ============================================================================
127
+ // Hash Generation
128
+ // ============================================================================
129
+
130
+ /**
131
+ * Generate deterministic hash for a single node
132
+ * Hash = SHA-256(type + sortedProps + childHashes)
133
+ */
134
+ export async function hashNodeAsync(
135
+ type: string,
136
+ props: Record<string, unknown>,
137
+ childHashes: string[],
138
+ ): Promise<string> {
139
+ const content = JSON.stringify({
140
+ type,
141
+ props: sortKeys(sanitizeProps(props)),
142
+ children: childHashes.sort(), // Sort for consistency
143
+ });
144
+
145
+ const fullHash = await sha256(content);
146
+ return truncateHash(fullHash);
147
+ }
148
+
149
+ /**
150
+ * Synchronous hash generation using djb2
151
+ * Faster but less collision-resistant
152
+ */
153
+ export function hashNodeSync(
154
+ type: string,
155
+ props: Record<string, unknown>,
156
+ childHashes: string[],
157
+ ): string {
158
+ const content = JSON.stringify({
159
+ type,
160
+ props: sortKeys(sanitizeProps(props)),
161
+ children: childHashes.sort(),
162
+ });
163
+
164
+ return djb2Hash(content);
165
+ }
166
+
167
+ // ============================================================================
168
+ // Merkle Tree Builder
169
+ // ============================================================================
170
+
171
+ interface BuildContext {
172
+ nodes: Map<string, MerkleNode>;
173
+ hashToId: Map<string, string>;
174
+ tree: ComponentTree;
175
+ }
176
+
177
+ /**
178
+ * Recursively build MerkleNode for a node and its children
179
+ * Uses post-order traversal (children first, then parent)
180
+ */
181
+ async function buildNodeAsync(
182
+ nodeId: string,
183
+ parentPath: string[],
184
+ parentPathHashes: string[],
185
+ depth: number,
186
+ ctx: BuildContext,
187
+ ): Promise<MerkleNode | null> {
188
+ const node = ctx.tree.getNode(nodeId);
189
+ if (!node) return null;
190
+
191
+ // Process children first (post-order)
192
+ const childHashes: string[] = [];
193
+ const children = ctx.tree.getChildren(nodeId);
194
+
195
+ for (const child of children) {
196
+ const childNode = await buildNodeAsync(
197
+ child.id,
198
+ [...parentPath, node.type],
199
+ [...parentPathHashes], // Will be filled after we compute our hash
200
+ depth + 1,
201
+ ctx,
202
+ );
203
+ if (childNode) {
204
+ childHashes.push(childNode.hash);
205
+ }
206
+ }
207
+
208
+ // Now hash this node
209
+ const hash = await hashNodeAsync(node.type, node.props || {}, childHashes);
210
+
211
+ // Build path with hashes
212
+ const path = [...parentPath, node.type];
213
+ const pathHashes = [...parentPathHashes, hash];
214
+
215
+ const merkleNode: MerkleNode = {
216
+ hash,
217
+ originalId: nodeId,
218
+ type: node.type,
219
+ props: sanitizeProps(node.props),
220
+ childHashes,
221
+ path,
222
+ pathHashes,
223
+ depth,
224
+ };
225
+
226
+ ctx.nodes.set(nodeId, merkleNode);
227
+ ctx.hashToId.set(hash, nodeId);
228
+
229
+ // Update children with correct parent path hashes
230
+ for (const child of children) {
231
+ const childMerkle = ctx.nodes.get(child.id);
232
+ if (childMerkle) {
233
+ childMerkle.pathHashes = [...pathHashes, childMerkle.hash];
234
+ }
235
+ }
236
+
237
+ return merkleNode;
238
+ }
239
+
240
+ /**
241
+ * Synchronous version for server-side rendering
242
+ */
243
+ function buildNodeSync(
244
+ nodeId: string,
245
+ parentPath: string[],
246
+ parentPathHashes: string[],
247
+ depth: number,
248
+ ctx: BuildContext,
249
+ ): MerkleNode | null {
250
+ const node = ctx.tree.getNode(nodeId);
251
+ if (!node) return null;
252
+
253
+ // Process children first (post-order)
254
+ const childHashes: string[] = [];
255
+ const children = ctx.tree.getChildren(nodeId);
256
+
257
+ for (const child of children) {
258
+ const childNode = buildNodeSync(
259
+ child.id,
260
+ [...parentPath, node.type],
261
+ [...parentPathHashes],
262
+ depth + 1,
263
+ ctx,
264
+ );
265
+ if (childNode) {
266
+ childHashes.push(childNode.hash);
267
+ }
268
+ }
269
+
270
+ // Hash this node
271
+ const hash = hashNodeSync(node.type, node.props || {}, childHashes);
272
+
273
+ // Build path with hashes
274
+ const path = [...parentPath, node.type];
275
+ const pathHashes = [...parentPathHashes, hash];
276
+
277
+ const merkleNode: MerkleNode = {
278
+ hash,
279
+ originalId: nodeId,
280
+ type: node.type,
281
+ props: sanitizeProps(node.props),
282
+ childHashes,
283
+ path,
284
+ pathHashes,
285
+ depth,
286
+ };
287
+
288
+ ctx.nodes.set(nodeId, merkleNode);
289
+ ctx.hashToId.set(hash, nodeId);
290
+
291
+ // Update children with correct parent path hashes
292
+ for (const child of children) {
293
+ const childMerkle = ctx.nodes.get(child.id);
294
+ if (childMerkle) {
295
+ childMerkle.pathHashes = [...pathHashes, childMerkle.hash];
296
+ }
297
+ }
298
+
299
+ return merkleNode;
300
+ }
301
+
302
+ /**
303
+ * Build complete Merkle tree from ComponentTree (async)
304
+ */
305
+ export async function buildMerkleTree(
306
+ tree: ComponentTree,
307
+ ): Promise<MerkleTree> {
308
+ const ctx: BuildContext = {
309
+ nodes: new Map(),
310
+ hashToId: new Map(),
311
+ tree,
312
+ };
313
+
314
+ const rootNode = await buildNodeAsync(tree.rootId, [], [], 0, ctx);
315
+
316
+ const merkleTree: MerkleTree = {
317
+ rootHash: rootNode?.hash || '',
318
+ nodes: ctx.nodes,
319
+
320
+ getNode(id: string): MerkleNode | undefined {
321
+ return ctx.nodes.get(id);
322
+ },
323
+
324
+ getNodeByHash(hash: string): MerkleNode | undefined {
325
+ const id = ctx.hashToId.get(hash);
326
+ return id ? ctx.nodes.get(id) : undefined;
327
+ },
328
+
329
+ getNodesAtDepth(depth: number): MerkleNode[] {
330
+ return Array.from(ctx.nodes.values()).filter((n) => n.depth === depth);
331
+ },
332
+ };
333
+
334
+ return merkleTree;
335
+ }
336
+
337
+ /**
338
+ * Build complete Merkle tree from ComponentTree (sync)
339
+ */
340
+ export function buildMerkleTreeSync(tree: ComponentTree): MerkleTree {
341
+ const ctx: BuildContext = {
342
+ nodes: new Map(),
343
+ hashToId: new Map(),
344
+ tree,
345
+ };
346
+
347
+ const rootNode = buildNodeSync(tree.rootId, [], [], 0, ctx);
348
+
349
+ const merkleTree: MerkleTree = {
350
+ rootHash: rootNode?.hash || '',
351
+ nodes: ctx.nodes,
352
+
353
+ getNode(id: string): MerkleNode | undefined {
354
+ return ctx.nodes.get(id);
355
+ },
356
+
357
+ getNodeByHash(hash: string): MerkleNode | undefined {
358
+ const id = ctx.hashToId.get(hash);
359
+ return id ? ctx.nodes.get(id) : undefined;
360
+ },
361
+
362
+ getNodesAtDepth(depth: number): MerkleNode[] {
363
+ return Array.from(ctx.nodes.values()).filter((n) => n.depth === depth);
364
+ },
365
+ };
366
+
367
+ return merkleTree;
368
+ }
369
+
370
+ // ============================================================================
371
+ // DOM Attribute Helpers
372
+ // ============================================================================
373
+
374
+ /**
375
+ * Generate data attributes for a DOM element
376
+ */
377
+ export function getMerkleAttributes(
378
+ merkleNode: MerkleNode,
379
+ ): Record<string, string> {
380
+ return {
381
+ 'data-aeon-merkle': merkleNode.hash,
382
+ 'data-aeon-path': JSON.stringify(merkleNode.path),
383
+ 'data-aeon-path-hashes': JSON.stringify(merkleNode.pathHashes),
384
+ 'data-aeon-type': merkleNode.type,
385
+ };
386
+ }
387
+
388
+ /**
389
+ * Parse Merkle info from DOM element
390
+ */
391
+ export function parseMerkleFromElement(element: HTMLElement): {
392
+ hash: string;
393
+ path: string[];
394
+ pathHashes: string[];
395
+ type: string;
396
+ } | null {
397
+ const hash = element.getAttribute('data-aeon-merkle');
398
+ if (!hash) return null;
399
+
400
+ const pathStr = element.getAttribute('data-aeon-path');
401
+ const pathHashesStr = element.getAttribute('data-aeon-path-hashes');
402
+ const type = element.getAttribute('data-aeon-type') || 'unknown';
403
+
404
+ let path: string[] = [];
405
+ let pathHashes: string[] = [];
406
+
407
+ try {
408
+ if (pathStr) path = JSON.parse(pathStr);
409
+ if (pathHashesStr) pathHashes = JSON.parse(pathHashesStr);
410
+ } catch {
411
+ // Invalid JSON, use empty arrays
412
+ }
413
+
414
+ return { hash, path, pathHashes, type };
415
+ }
416
+
417
+ /**
418
+ * Find nearest ancestor with Merkle attributes
419
+ */
420
+ export function findNearestMerkleElement(
421
+ element: HTMLElement,
422
+ ): HTMLElement | null {
423
+ let current: HTMLElement | null = element;
424
+
425
+ while (current) {
426
+ if (current.hasAttribute('data-aeon-merkle')) {
427
+ return current;
428
+ }
429
+ current = current.parentElement;
430
+ }
431
+
432
+ return null;
433
+ }
434
+
435
+ // ============================================================================
436
+ // Verification Utilities
437
+ // ============================================================================
438
+
439
+ /**
440
+ * Verify tree integrity by recomputing root hash
441
+ */
442
+ export async function verifyMerkleTree(
443
+ tree: MerkleTree,
444
+ componentTree: ComponentTree,
445
+ ): Promise<boolean> {
446
+ const rebuilt = await buildMerkleTree(componentTree);
447
+ return rebuilt.rootHash === tree.rootHash;
448
+ }
449
+
450
+ /**
451
+ * Find nodes that have changed between two Merkle trees
452
+ */
453
+ export function diffMerkleTrees(
454
+ oldTree: MerkleTree,
455
+ newTree: MerkleTree,
456
+ ): { added: MerkleNode[]; removed: MerkleNode[]; changed: MerkleNode[] } {
457
+ const added: MerkleNode[] = [];
458
+ const removed: MerkleNode[] = [];
459
+ const changed: MerkleNode[] = [];
460
+
461
+ const oldHashes = new Set(
462
+ Array.from(oldTree.nodes.values()).map((n) => n.hash),
463
+ );
464
+ const newHashes = new Set(
465
+ Array.from(newTree.nodes.values()).map((n) => n.hash),
466
+ );
467
+
468
+ // Find added nodes (in new but not in old)
469
+ for (const [, node] of newTree.nodes) {
470
+ if (!oldHashes.has(node.hash)) {
471
+ // Check if originalId exists in old tree (changed) or not (added)
472
+ const oldNode = oldTree.getNode(node.originalId);
473
+ if (oldNode) {
474
+ changed.push(node);
475
+ } else {
476
+ added.push(node);
477
+ }
478
+ }
479
+ }
480
+
481
+ // Find removed nodes (in old but not in new by ID)
482
+ for (const [id, node] of oldTree.nodes) {
483
+ if (!newTree.getNode(id)) {
484
+ removed.push(node);
485
+ }
486
+ }
487
+
488
+ return { added, removed, changed };
489
+ }