@affectively/aeon-pages 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
|
@@ -0,0 +1,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
|
+
}
|