@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,478 @@
1
+ /**
2
+ * Aeon Pages Encrypted Offline Queue
3
+ *
4
+ * Persistent, encrypted operation queue for offline-first applications.
5
+ *
6
+ * Features:
7
+ * - Priority queuing (high/normal/low)
8
+ * - Configurable local capacity with overflow handling
9
+ * - Optional UCAN-based or session-based encryption
10
+ * - Automatic cleanup of synced operations
11
+ * - O(1) lookups via Map index
12
+ * - Works with any storage adapter
13
+ */
14
+
15
+ import type { StorageAdapter } from '../storage';
16
+ import type {
17
+ OfflineOperation,
18
+ EncryptedQueueConfig,
19
+ QueueStats,
20
+ EncryptionKeyMaterial,
21
+ } from './types';
22
+ import {
23
+ OfflineOperationEncryption,
24
+ getOperationEncryption,
25
+ generateOperationId,
26
+ estimateEncryptedSize,
27
+ } from './encryption';
28
+
29
+ // ============================================================================
30
+ // Default Configuration
31
+ // ============================================================================
32
+
33
+ const DEFAULT_CONFIG: EncryptedQueueConfig = {
34
+ maxLocalCapacity: 50 * 1024 * 1024, // 50MB
35
+ compactionThreshold: 0.8,
36
+ d1SyncInterval: 5 * 60 * 1000, // 5 minutes
37
+ syncedCleanupAge: 60 * 60 * 1000, // 1 hour
38
+ encryption: {
39
+ enabled: false,
40
+ keyDerivation: 'session',
41
+ },
42
+ };
43
+
44
+ // ============================================================================
45
+ // Event Emitter (minimal implementation)
46
+ // ============================================================================
47
+
48
+ type EventHandler = (data: unknown) => void;
49
+
50
+ class OfflineQueueEventEmitter {
51
+ private handlers = new Map<string, Set<EventHandler>>();
52
+
53
+ on(event: string, handler: EventHandler): void {
54
+ if (!this.handlers.has(event)) {
55
+ this.handlers.set(event, new Set());
56
+ }
57
+ this.handlers.get(event)!.add(handler);
58
+ }
59
+
60
+ off(event: string, handler: EventHandler): void {
61
+ this.handlers.get(event)?.delete(handler);
62
+ }
63
+
64
+ emit(event: string, data?: unknown): void {
65
+ this.handlers.get(event)?.forEach((handler) => handler(data));
66
+ }
67
+ }
68
+
69
+ // ============================================================================
70
+ // Encrypted Offline Queue
71
+ // ============================================================================
72
+
73
+ export class EncryptedOfflineQueue extends OfflineQueueEventEmitter {
74
+ private config: EncryptedQueueConfig;
75
+ private operations: Map<string, OfflineOperation> = new Map();
76
+ private isInitialized = false;
77
+ private cleanupTimer: ReturnType<typeof setInterval> | null = null;
78
+ private currentBytes = 0;
79
+ private encryption: OfflineOperationEncryption;
80
+ private keyMaterial: EncryptionKeyMaterial | null = null;
81
+ private storage: StorageAdapter | null = null;
82
+
83
+ constructor(config: Partial<EncryptedQueueConfig> = {}) {
84
+ super();
85
+ this.config = { ...DEFAULT_CONFIG, ...config };
86
+ this.encryption = getOperationEncryption();
87
+ }
88
+
89
+ /**
90
+ * Initialize the queue
91
+ */
92
+ async initialize(options?: {
93
+ storage?: StorageAdapter;
94
+ keyMaterial?: EncryptionKeyMaterial;
95
+ }): Promise<void> {
96
+ if (this.isInitialized) return;
97
+
98
+ this.storage = options?.storage ?? null;
99
+ this.keyMaterial = options?.keyMaterial ?? null;
100
+
101
+ // Load existing operations from storage if available
102
+ if (this.storage) {
103
+ await this.loadFromStorage();
104
+ }
105
+
106
+ // Start cleanup timer
107
+ this.startCleanupTimer();
108
+
109
+ this.isInitialized = true;
110
+ this.emit('initialized');
111
+ }
112
+
113
+ /**
114
+ * Set encryption key material (can be updated at runtime)
115
+ */
116
+ setKeyMaterial(keyMaterial: EncryptionKeyMaterial): void {
117
+ this.keyMaterial = keyMaterial;
118
+ }
119
+
120
+ /**
121
+ * Queue an operation
122
+ */
123
+ async queueOperation(
124
+ operation: Omit<
125
+ OfflineOperation,
126
+ | 'id'
127
+ | 'status'
128
+ | 'encryptedData'
129
+ | 'bytesSize'
130
+ | 'failedCount'
131
+ | 'retryCount'
132
+ | 'maxRetries'
133
+ >,
134
+ ): Promise<string> {
135
+ if (!this.isInitialized) {
136
+ throw new Error('Queue not initialized');
137
+ }
138
+
139
+ const operationId = generateOperationId();
140
+ let encryptedData: Uint8Array | undefined;
141
+ let size: number;
142
+
143
+ // Encrypt if enabled and key material available
144
+ if (this.config.encryption.enabled && this.keyMaterial) {
145
+ encryptedData = await this.encryption.encryptOperation(
146
+ operation,
147
+ this.keyMaterial,
148
+ );
149
+ size = encryptedData.byteLength;
150
+ } else {
151
+ size = estimateEncryptedSize(operation);
152
+ }
153
+
154
+ // Check capacity
155
+ if (this.currentBytes + size > this.config.maxLocalCapacity) {
156
+ await this.compactQueue();
157
+
158
+ if (this.currentBytes + size > this.config.maxLocalCapacity) {
159
+ const error = 'Queue capacity exceeded';
160
+ this.emit('queue:error', { operationId, error });
161
+ throw new Error(error);
162
+ }
163
+ }
164
+
165
+ const fullOperation: OfflineOperation = {
166
+ id: operationId,
167
+ type: operation.type,
168
+ sessionId: operation.sessionId,
169
+ status: 'pending',
170
+ data: operation.data,
171
+ priority: operation.priority || 'normal',
172
+ encryptedData,
173
+ encryptionVersion: 1,
174
+ bytesSize: size,
175
+ createdAt: operation.createdAt || Date.now(),
176
+ failedCount: 0,
177
+ retryCount: 0,
178
+ maxRetries: 5,
179
+ };
180
+
181
+ this.operations.set(operationId, fullOperation);
182
+ this.currentBytes += size;
183
+
184
+ this.emit('operation:queued', {
185
+ operationId,
186
+ sessionId: operation.sessionId,
187
+ size,
188
+ });
189
+
190
+ return operationId;
191
+ }
192
+
193
+ /**
194
+ * Get pending operations ready for sync
195
+ */
196
+ getPendingOperations(sessionId?: string, limit = 100): OfflineOperation[] {
197
+ if (!this.isInitialized) {
198
+ throw new Error('Queue not initialized');
199
+ }
200
+
201
+ const pending: OfflineOperation[] = [];
202
+
203
+ Array.from(this.operations.values()).forEach((op) => {
204
+ if (op.status !== 'pending') return;
205
+ if (sessionId && op.sessionId !== sessionId) return;
206
+ pending.push(op);
207
+ });
208
+
209
+ // Sort by priority (high > normal > low) then by createdAt
210
+ pending.sort((a, b) => {
211
+ const priorityOrder = { high: 0, normal: 1, low: 2 };
212
+ const priorityDiff =
213
+ priorityOrder[a.priority] - priorityOrder[b.priority];
214
+ if (priorityDiff !== 0) return priorityDiff;
215
+ return a.createdAt - b.createdAt;
216
+ });
217
+
218
+ return pending.slice(0, limit);
219
+ }
220
+
221
+ /**
222
+ * Get decrypted operation data
223
+ */
224
+ async getDecryptedOperation(
225
+ operationId: string,
226
+ ): Promise<Omit<
227
+ OfflineOperation,
228
+ | 'id'
229
+ | 'status'
230
+ | 'encryptedData'
231
+ | 'bytesSize'
232
+ | 'failedCount'
233
+ | 'retryCount'
234
+ | 'maxRetries'
235
+ > | null> {
236
+ const op = this.operations.get(operationId);
237
+ if (!op) return null;
238
+
239
+ if (op.encryptedData && this.keyMaterial) {
240
+ return this.encryption.decryptOperation(
241
+ op.encryptedData,
242
+ this.keyMaterial,
243
+ );
244
+ }
245
+
246
+ return {
247
+ type: op.type,
248
+ sessionId: op.sessionId,
249
+ data: op.data,
250
+ priority: op.priority,
251
+ createdAt: op.createdAt,
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Mark operation as syncing
257
+ */
258
+ markSyncing(operationId: string): void {
259
+ if (!this.isInitialized) {
260
+ throw new Error('Queue not initialized');
261
+ }
262
+
263
+ const op = this.operations.get(operationId);
264
+ if (op) {
265
+ op.status = 'syncing';
266
+ this.emit('operation:syncing', { operationId });
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Mark operation as synced
272
+ */
273
+ markSynced(operationId: string): void {
274
+ if (!this.isInitialized) {
275
+ throw new Error('Queue not initialized');
276
+ }
277
+
278
+ const op = this.operations.get(operationId);
279
+ if (op) {
280
+ op.status = 'synced';
281
+ op.syncedAt = Date.now();
282
+ op.failedCount = 0;
283
+ this.emit('operation:synced', { operationId });
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Mark operation as failed with retry logic
289
+ */
290
+ markFailed(operationId: string, error: string): void {
291
+ if (!this.isInitialized) {
292
+ throw new Error('Queue not initialized');
293
+ }
294
+
295
+ const op = this.operations.get(operationId);
296
+ if (!op) return;
297
+
298
+ op.failedCount += 1;
299
+ op.lastError = error;
300
+ op.retryCount += 1;
301
+
302
+ if (op.failedCount >= op.maxRetries) {
303
+ op.status = 'failed';
304
+ this.emit('operation:failed_max_retries', { operationId, error });
305
+ } else {
306
+ op.status = 'pending'; // Reset to pending for retry
307
+ this.emit('operation:retry', { operationId, attempt: op.failedCount });
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Remove an operation from the queue
313
+ */
314
+ removeOperation(operationId: string): boolean {
315
+ const op = this.operations.get(operationId);
316
+ if (op) {
317
+ this.currentBytes -= op.bytesSize;
318
+ this.operations.delete(operationId);
319
+ return true;
320
+ }
321
+ return false;
322
+ }
323
+
324
+ /**
325
+ * Get queue statistics
326
+ */
327
+ getStats(): QueueStats {
328
+ if (!this.isInitialized) {
329
+ return {
330
+ total: 0,
331
+ pending: 0,
332
+ syncing: 0,
333
+ synced: 0,
334
+ failed: 0,
335
+ totalBytes: 0,
336
+ compactionNeeded: false,
337
+ };
338
+ }
339
+
340
+ let pending = 0;
341
+ let syncing = 0;
342
+ let synced = 0;
343
+ let failed = 0;
344
+
345
+ Array.from(this.operations.values()).forEach((op) => {
346
+ switch (op.status) {
347
+ case 'pending':
348
+ pending++;
349
+ break;
350
+ case 'syncing':
351
+ syncing++;
352
+ break;
353
+ case 'synced':
354
+ synced++;
355
+ break;
356
+ case 'failed':
357
+ failed++;
358
+ break;
359
+ }
360
+ });
361
+
362
+ const compactionNeeded =
363
+ this.currentBytes / this.config.maxLocalCapacity >
364
+ this.config.compactionThreshold;
365
+
366
+ return {
367
+ total: this.operations.size,
368
+ pending,
369
+ syncing,
370
+ synced,
371
+ failed,
372
+ totalBytes: this.currentBytes,
373
+ compactionNeeded,
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Clear all operations
379
+ */
380
+ clear(): void {
381
+ this.operations.clear();
382
+ this.currentBytes = 0;
383
+ }
384
+
385
+ /**
386
+ * Compact queue by removing old synced operations
387
+ */
388
+ private async compactQueue(): Promise<void> {
389
+ const cutoff = Date.now() - this.config.syncedCleanupAge;
390
+ const toRemove: string[] = [];
391
+
392
+ Array.from(this.operations.entries()).forEach(([id, op]) => {
393
+ if (op.status === 'synced' && op.syncedAt && op.syncedAt < cutoff) {
394
+ toRemove.push(id);
395
+ }
396
+ });
397
+
398
+ for (const id of toRemove) {
399
+ this.removeOperation(id);
400
+ }
401
+
402
+ if (toRemove.length > 0) {
403
+ this.emit('queue:compacted');
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Load operations from storage
409
+ */
410
+ private async loadFromStorage(): Promise<void> {
411
+ // Storage loading would be implemented based on the storage adapter
412
+ // For now, this is a no-op as we're using in-memory storage
413
+ // In a real implementation, this would load from IndexedDB or D1
414
+ }
415
+
416
+ /**
417
+ * Start automatic cleanup timer
418
+ */
419
+ private startCleanupTimer(): void {
420
+ if (this.cleanupTimer) {
421
+ clearInterval(this.cleanupTimer);
422
+ }
423
+
424
+ this.cleanupTimer = setInterval(async () => {
425
+ const stats = this.getStats();
426
+ if (stats.compactionNeeded) {
427
+ await this.compactQueue();
428
+ }
429
+ }, 60000); // Check every minute
430
+ }
431
+
432
+ /**
433
+ * Shutdown the queue
434
+ */
435
+ shutdown(): void {
436
+ if (this.cleanupTimer) {
437
+ clearInterval(this.cleanupTimer);
438
+ this.cleanupTimer = null;
439
+ }
440
+ this.isInitialized = false;
441
+ this.emit('shutdown');
442
+ }
443
+ }
444
+
445
+ // ============================================================================
446
+ // Singleton Instance
447
+ // ============================================================================
448
+
449
+ let _queueInstance: EncryptedOfflineQueue | null = null;
450
+
451
+ /**
452
+ * Get the singleton queue instance
453
+ */
454
+ export function getOfflineQueue(): EncryptedOfflineQueue {
455
+ if (!_queueInstance) {
456
+ _queueInstance = new EncryptedOfflineQueue();
457
+ }
458
+ return _queueInstance;
459
+ }
460
+
461
+ /**
462
+ * Create a new queue with custom configuration
463
+ */
464
+ export function createOfflineQueue(
465
+ config?: Partial<EncryptedQueueConfig>,
466
+ ): EncryptedOfflineQueue {
467
+ return new EncryptedOfflineQueue(config);
468
+ }
469
+
470
+ /**
471
+ * Reset the singleton queue (for testing)
472
+ */
473
+ export function resetOfflineQueue(): void {
474
+ if (_queueInstance) {
475
+ _queueInstance.shutdown();
476
+ }
477
+ _queueInstance = null;
478
+ }