@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,596 @@
1
+ /**
2
+ * Aeon Pages Sync Coordinator
3
+ *
4
+ * Coordinates synchronization of offline operations with server.
5
+ * Handles bandwidth optimization, batching, and network state management.
6
+ *
7
+ * Features:
8
+ * - Adaptive batch sizing based on network conditions
9
+ * - Network state tracking and monitoring
10
+ * - Bandwidth profiling
11
+ * - Sync progress tracking
12
+ * - Configurable compression and delta sync
13
+ */
14
+
15
+ import type {
16
+ OfflineOperation,
17
+ SyncBatch,
18
+ SyncResult,
19
+ SyncCoordinatorConfig,
20
+ SyncProgressEvent,
21
+ NetworkState,
22
+ NetworkStateEvent,
23
+ BandwidthProfile,
24
+ OperationPriority,
25
+ } from '../offline/types';
26
+
27
+ // ============================================================================
28
+ // Types
29
+ // ============================================================================
30
+
31
+ export interface SyncStats {
32
+ totalSyncsAttempted: number;
33
+ successfulSyncs: number;
34
+ failedSyncs: number;
35
+ totalOperationsSynced: number;
36
+ averageSyncDurationMs: number;
37
+ lastSyncTime?: number;
38
+ networkStateHistory: Array<{ state: NetworkState; timestamp: number }>;
39
+ bandwidthHistory: BandwidthProfile[];
40
+ }
41
+
42
+ // ============================================================================
43
+ // Event Emitter (minimal implementation)
44
+ // ============================================================================
45
+
46
+ type EventHandler<T> = (data: T) => void;
47
+
48
+ class EventEmitter<
49
+ Events extends Record<string, unknown> = Record<string, unknown>,
50
+ > {
51
+ private handlers = new Map<string, Set<EventHandler<unknown>>>();
52
+
53
+ on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void {
54
+ const key = event as string;
55
+ if (!this.handlers.has(key)) {
56
+ this.handlers.set(key, new Set());
57
+ }
58
+ this.handlers.get(key)!.add(handler as EventHandler<unknown>);
59
+ }
60
+
61
+ off<K extends keyof Events>(
62
+ event: K,
63
+ handler: EventHandler<Events[K]>,
64
+ ): void {
65
+ this.handlers
66
+ .get(event as string)
67
+ ?.delete(handler as EventHandler<unknown>);
68
+ }
69
+
70
+ emit<K extends keyof Events>(event: K, data?: Events[K]): void {
71
+ this.handlers.get(event as string)?.forEach((handler) => handler(data));
72
+ }
73
+ }
74
+
75
+ // ============================================================================
76
+ // Default Configuration
77
+ // ============================================================================
78
+
79
+ const DEFAULT_CONFIG: SyncCoordinatorConfig = {
80
+ maxBatchSize: 100,
81
+ maxBatchBytes: 5 * 1024 * 1024, // 5MB
82
+ batchTimeoutMs: 5000,
83
+ maxRetries: 5,
84
+ retryDelayMs: 1000,
85
+ enableCompression: true,
86
+ enableDeltaSync: true,
87
+ adaptiveBatching: true,
88
+ };
89
+
90
+ // ============================================================================
91
+ // Sync Coordinator
92
+ // ============================================================================
93
+
94
+ export class SyncCoordinator extends EventEmitter<{
95
+ 'network:changed': NetworkStateEvent;
96
+ 'network:online': void;
97
+ 'network:offline': void;
98
+ 'bandwidth:updated': BandwidthProfile;
99
+ 'batch:created': SyncBatch;
100
+ 'batch:started': { batchId: string };
101
+ 'batch:progress': SyncProgressEvent;
102
+ 'batch:completed': { batch: SyncBatch; result: SyncResult };
103
+ 'batch:failed': { batch: SyncBatch; error: string };
104
+ 'batch:retry': { batch: SyncBatch; attempt: number };
105
+ 'config:updated': SyncCoordinatorConfig;
106
+ }> {
107
+ private networkState: NetworkState = 'unknown';
108
+ private bandwidthProfile: BandwidthProfile = {
109
+ speedKbps: 1024,
110
+ latencyMs: 50,
111
+ timestamp: Date.now(),
112
+ reliability: 1,
113
+ effectiveType: 'unknown',
114
+ };
115
+
116
+ private batches: Map<string, SyncBatch> = new Map();
117
+ private progress: Map<string, SyncProgressEvent> = new Map();
118
+ private currentSyncBatchId: string | null = null;
119
+ private config: SyncCoordinatorConfig;
120
+ private syncTimings: number[] = [];
121
+
122
+ private stats: SyncStats = {
123
+ totalSyncsAttempted: 0,
124
+ successfulSyncs: 0,
125
+ failedSyncs: 0,
126
+ totalOperationsSynced: 0,
127
+ averageSyncDurationMs: 0,
128
+ networkStateHistory: [],
129
+ bandwidthHistory: [],
130
+ };
131
+
132
+ constructor(config: Partial<SyncCoordinatorConfig> = {}) {
133
+ super();
134
+ this.config = { ...DEFAULT_CONFIG, ...config };
135
+
136
+ // Initialize network state detection
137
+ if (typeof navigator !== 'undefined') {
138
+ this.initNetworkDetection();
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Initialize network state detection
144
+ */
145
+ private initNetworkDetection(): void {
146
+ // Check initial state
147
+ if (typeof navigator !== 'undefined' && 'onLine' in navigator) {
148
+ this.setNetworkState(navigator.onLine ? 'online' : 'offline');
149
+ }
150
+
151
+ // Listen for changes
152
+ if (typeof window !== 'undefined') {
153
+ window.addEventListener('online', () => this.setNetworkState('online'));
154
+ window.addEventListener('offline', () => this.setNetworkState('offline'));
155
+ }
156
+
157
+ // Check connection quality if available
158
+ if (typeof navigator !== 'undefined' && 'connection' in navigator) {
159
+ const conn = (
160
+ navigator as Navigator & { connection?: NetworkInformation }
161
+ ).connection;
162
+ if (conn) {
163
+ this.updateBandwidthFromConnection(conn);
164
+ conn.addEventListener?.('change', () =>
165
+ this.updateBandwidthFromConnection(conn),
166
+ );
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Update bandwidth profile from Network Information API
173
+ */
174
+ private updateBandwidthFromConnection(conn: NetworkInformation): void {
175
+ const effectiveType =
176
+ conn.effectiveType as BandwidthProfile['effectiveType'];
177
+
178
+ let speedKbps = 1024; // Default 1 Mbps
179
+ let latencyMs = 50;
180
+
181
+ switch (effectiveType) {
182
+ case 'slow-2g':
183
+ speedKbps = 50;
184
+ latencyMs = 2000;
185
+ break;
186
+ case '2g':
187
+ speedKbps = 150;
188
+ latencyMs = 1000;
189
+ break;
190
+ case '3g':
191
+ speedKbps = 750;
192
+ latencyMs = 400;
193
+ break;
194
+ case '4g':
195
+ speedKbps = 5000;
196
+ latencyMs = 50;
197
+ break;
198
+ }
199
+
200
+ // Use actual downlink if available
201
+ if (conn.downlink) {
202
+ speedKbps = conn.downlink * 1024; // Convert Mbps to Kbps
203
+ }
204
+
205
+ // Use RTT if available
206
+ if (conn.rtt) {
207
+ latencyMs = conn.rtt;
208
+ }
209
+
210
+ this.updateBandwidthProfile({
211
+ speedKbps,
212
+ latencyMs,
213
+ effectiveType,
214
+ reliability:
215
+ effectiveType === '4g' ? 0.95 : effectiveType === '3g' ? 0.85 : 0.7,
216
+ });
217
+
218
+ // Update network state based on connection quality
219
+ if (effectiveType === 'slow-2g' || effectiveType === '2g') {
220
+ this.setNetworkState('poor');
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Update network state
226
+ */
227
+ setNetworkState(state: NetworkState): void {
228
+ const previousState = this.networkState;
229
+ if (previousState === state) return;
230
+
231
+ this.networkState = state;
232
+
233
+ const event: NetworkStateEvent = {
234
+ previousState,
235
+ newState: state,
236
+ bandwidth: this.bandwidthProfile,
237
+ timestamp: Date.now(),
238
+ };
239
+
240
+ this.stats.networkStateHistory.push({ state, timestamp: Date.now() });
241
+ if (this.stats.networkStateHistory.length > 100) {
242
+ this.stats.networkStateHistory.shift();
243
+ }
244
+
245
+ this.emit('network:changed', event);
246
+
247
+ if (previousState !== 'online' && state === 'online') {
248
+ this.emit('network:online');
249
+ } else if (previousState === 'online' && state !== 'online') {
250
+ this.emit('network:offline');
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Get current network state
256
+ */
257
+ getNetworkState(): NetworkState {
258
+ return this.networkState;
259
+ }
260
+
261
+ /**
262
+ * Update bandwidth profile
263
+ */
264
+ updateBandwidthProfile(profile: Partial<BandwidthProfile>): void {
265
+ this.bandwidthProfile = {
266
+ ...this.bandwidthProfile,
267
+ ...profile,
268
+ timestamp: Date.now(),
269
+ };
270
+
271
+ this.stats.bandwidthHistory.push(this.bandwidthProfile);
272
+ if (this.stats.bandwidthHistory.length > 50) {
273
+ this.stats.bandwidthHistory.shift();
274
+ }
275
+
276
+ // Adapt batch sizes if enabled
277
+ if (this.config.adaptiveBatching) {
278
+ this.adaptBatchSizes();
279
+ }
280
+
281
+ this.emit('bandwidth:updated', this.bandwidthProfile);
282
+ }
283
+
284
+ /**
285
+ * Get current bandwidth profile
286
+ */
287
+ getBandwidthProfile(): BandwidthProfile {
288
+ return { ...this.bandwidthProfile };
289
+ }
290
+
291
+ /**
292
+ * Create a sync batch from operations
293
+ */
294
+ createSyncBatch(operations: OfflineOperation[]): SyncBatch {
295
+ // Limit to max batch size
296
+ const batchOps = operations.slice(0, this.config.maxBatchSize);
297
+
298
+ // Calculate total size
299
+ let totalSize = 0;
300
+ const sizedOps: OfflineOperation[] = [];
301
+
302
+ for (const op of batchOps) {
303
+ const opSize = op.bytesSize || JSON.stringify(op).length;
304
+ if (totalSize + opSize > this.config.maxBatchBytes) {
305
+ break;
306
+ }
307
+ totalSize += opSize;
308
+ sizedOps.push(op);
309
+ }
310
+
311
+ // Determine highest priority in batch
312
+ const priorityOrder: Record<OperationPriority, number> = {
313
+ high: 0,
314
+ normal: 1,
315
+ low: 2,
316
+ };
317
+ const highestPriority = sizedOps.reduce<OperationPriority>(
318
+ (highest, op) =>
319
+ priorityOrder[op.priority] < priorityOrder[highest]
320
+ ? op.priority
321
+ : highest,
322
+ 'low',
323
+ );
324
+
325
+ const batch: SyncBatch = {
326
+ batchId: `batch-${Date.now()}-${Math.random().toString(36).slice(2)}`,
327
+ operations: sizedOps,
328
+ totalSize,
329
+ createdAt: Date.now(),
330
+ priority: highestPriority,
331
+ compressed: this.config.enableCompression,
332
+ };
333
+
334
+ this.batches.set(batch.batchId, batch);
335
+ this.emit('batch:created', batch);
336
+
337
+ return batch;
338
+ }
339
+
340
+ /**
341
+ * Start syncing a batch
342
+ */
343
+ startSyncBatch(batchId: string): void {
344
+ const batch = this.batches.get(batchId);
345
+ if (!batch) return;
346
+
347
+ this.currentSyncBatchId = batchId;
348
+ this.stats.totalSyncsAttempted++;
349
+
350
+ this.progress.set(batchId, {
351
+ batchId,
352
+ totalOperations: batch.operations.length,
353
+ syncedOperations: 0,
354
+ bytesSynced: 0,
355
+ totalBytes: batch.totalSize,
356
+ });
357
+
358
+ this.emit('batch:started', { batchId });
359
+ }
360
+
361
+ /**
362
+ * Update sync progress
363
+ */
364
+ updateProgress(
365
+ batchId: string,
366
+ syncedOperations: number,
367
+ bytesSynced: number,
368
+ ): void {
369
+ const batch = this.batches.get(batchId);
370
+ if (!batch) return;
371
+
372
+ const progress: SyncProgressEvent = {
373
+ batchId,
374
+ totalOperations: batch.operations.length,
375
+ syncedOperations,
376
+ bytesSynced,
377
+ totalBytes: batch.totalSize,
378
+ estimatedTimeRemaining: this.estimateSyncTime(
379
+ batch.totalSize - bytesSynced,
380
+ ),
381
+ };
382
+
383
+ this.progress.set(batchId, progress);
384
+ this.emit('batch:progress', progress);
385
+ }
386
+
387
+ /**
388
+ * Complete a sync batch
389
+ */
390
+ completeSyncBatch(batchId: string, result: SyncResult): void {
391
+ const batch = this.batches.get(batchId);
392
+ if (!batch) return;
393
+
394
+ if (result.success) {
395
+ this.stats.successfulSyncs++;
396
+ this.stats.totalOperationsSynced += result.synced.length;
397
+ this.stats.lastSyncTime = Date.now();
398
+ } else {
399
+ this.stats.failedSyncs++;
400
+ }
401
+
402
+ this.currentSyncBatchId = null;
403
+ this.emit('batch:completed', { batch, result });
404
+ }
405
+
406
+ /**
407
+ * Mark batch as failed
408
+ */
409
+ failSyncBatch(batchId: string, error: string, retryable = true): void {
410
+ const batch = this.batches.get(batchId);
411
+ if (!batch) return;
412
+
413
+ const attemptCount =
414
+ (batch as SyncBatch & { attemptCount?: number }).attemptCount || 0;
415
+
416
+ if (retryable && attemptCount < this.config.maxRetries) {
417
+ (batch as SyncBatch & { attemptCount: number }).attemptCount =
418
+ attemptCount + 1;
419
+ this.emit('batch:retry', { batch, attempt: attemptCount + 1 });
420
+ } else {
421
+ this.stats.failedSyncs++;
422
+ this.emit('batch:failed', { batch, error });
423
+ }
424
+
425
+ this.currentSyncBatchId = null;
426
+ }
427
+
428
+ /**
429
+ * Get batch by ID
430
+ */
431
+ getBatch(batchId: string): SyncBatch | undefined {
432
+ return this.batches.get(batchId);
433
+ }
434
+
435
+ /**
436
+ * Get all pending batches
437
+ */
438
+ getPendingBatches(): SyncBatch[] {
439
+ return Array.from(this.batches.values());
440
+ }
441
+
442
+ /**
443
+ * Get current sync progress
444
+ */
445
+ getCurrentProgress(): SyncProgressEvent | undefined {
446
+ if (this.currentSyncBatchId) {
447
+ return this.progress.get(this.currentSyncBatchId);
448
+ }
449
+ return undefined;
450
+ }
451
+
452
+ /**
453
+ * Estimate sync time for given bytes
454
+ */
455
+ estimateSyncTime(bytes: number): number {
456
+ const secondsNeeded =
457
+ (bytes * 8) / (this.bandwidthProfile.speedKbps * 1024);
458
+ return Math.round(
459
+ (secondsNeeded + this.bandwidthProfile.latencyMs / 1000) * 1000,
460
+ );
461
+ }
462
+
463
+ /**
464
+ * Adapt batch sizes based on bandwidth
465
+ */
466
+ private adaptBatchSizes(): void {
467
+ const speed = this.bandwidthProfile.speedKbps;
468
+
469
+ // Poor connection - reduce batch size
470
+ if (speed < 512) {
471
+ this.config.maxBatchSize = Math.max(
472
+ 10,
473
+ Math.floor(DEFAULT_CONFIG.maxBatchSize / 4),
474
+ );
475
+ this.config.maxBatchBytes = Math.max(
476
+ 512 * 1024,
477
+ Math.floor(DEFAULT_CONFIG.maxBatchBytes / 4),
478
+ );
479
+ } else if (speed < 1024) {
480
+ this.config.maxBatchSize = Math.max(
481
+ 25,
482
+ Math.floor(DEFAULT_CONFIG.maxBatchSize / 2),
483
+ );
484
+ this.config.maxBatchBytes = Math.max(
485
+ 1024 * 1024,
486
+ Math.floor(DEFAULT_CONFIG.maxBatchBytes / 2),
487
+ );
488
+ } else if (speed > 5000) {
489
+ // Good connection - increase batch size
490
+ this.config.maxBatchSize = Math.min(500, DEFAULT_CONFIG.maxBatchSize * 2);
491
+ this.config.maxBatchBytes = Math.min(
492
+ 50 * 1024 * 1024,
493
+ DEFAULT_CONFIG.maxBatchBytes * 2,
494
+ );
495
+ } else {
496
+ // Normal connection - use defaults
497
+ this.config.maxBatchSize = DEFAULT_CONFIG.maxBatchSize;
498
+ this.config.maxBatchBytes = DEFAULT_CONFIG.maxBatchBytes;
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Get sync statistics
504
+ */
505
+ getStats(): SyncStats {
506
+ return { ...this.stats };
507
+ }
508
+
509
+ /**
510
+ * Configure the coordinator
511
+ */
512
+ configure(config: Partial<SyncCoordinatorConfig>): void {
513
+ this.config = { ...this.config, ...config };
514
+ this.emit('config:updated', this.config);
515
+ }
516
+
517
+ /**
518
+ * Get current configuration
519
+ */
520
+ getConfig(): SyncCoordinatorConfig {
521
+ return { ...this.config };
522
+ }
523
+
524
+ /**
525
+ * Clear all batches
526
+ */
527
+ clear(): void {
528
+ this.batches.clear();
529
+ this.progress.clear();
530
+ this.currentSyncBatchId = null;
531
+ }
532
+
533
+ /**
534
+ * Reset service (for testing)
535
+ */
536
+ reset(): void {
537
+ this.clear();
538
+ this.networkState = 'unknown';
539
+ this.syncTimings = [];
540
+ this.stats = {
541
+ totalSyncsAttempted: 0,
542
+ successfulSyncs: 0,
543
+ failedSyncs: 0,
544
+ totalOperationsSynced: 0,
545
+ averageSyncDurationMs: 0,
546
+ networkStateHistory: [],
547
+ bandwidthHistory: [],
548
+ };
549
+ }
550
+ }
551
+
552
+ // ============================================================================
553
+ // Network Information API types
554
+ // ============================================================================
555
+
556
+ interface NetworkInformation {
557
+ effectiveType?: string;
558
+ downlink?: number;
559
+ rtt?: number;
560
+ addEventListener?: (event: string, callback: () => void) => void;
561
+ }
562
+
563
+ // ============================================================================
564
+ // Singleton Instance
565
+ // ============================================================================
566
+
567
+ let _instance: SyncCoordinator | null = null;
568
+
569
+ /**
570
+ * Get the singleton sync coordinator instance
571
+ */
572
+ export function getSyncCoordinator(): SyncCoordinator {
573
+ if (!_instance) {
574
+ _instance = new SyncCoordinator();
575
+ }
576
+ return _instance;
577
+ }
578
+
579
+ /**
580
+ * Create a new sync coordinator with custom configuration
581
+ */
582
+ export function createSyncCoordinator(
583
+ config?: Partial<SyncCoordinatorConfig>,
584
+ ): SyncCoordinator {
585
+ return new SyncCoordinator(config);
586
+ }
587
+
588
+ /**
589
+ * Reset the singleton coordinator (for testing)
590
+ */
591
+ export function resetSyncCoordinator(): void {
592
+ if (_instance) {
593
+ _instance.reset();
594
+ }
595
+ _instance = null;
596
+ }