@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,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
|
+
}
|