@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,607 @@
1
+ /**
2
+ * Tests for Encrypted Offline Queue
3
+ */
4
+
5
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
6
+ import {
7
+ EncryptedOfflineQueue,
8
+ getOfflineQueue,
9
+ createOfflineQueue,
10
+ resetOfflineQueue,
11
+ } from './encrypted-queue';
12
+ import { getOperationEncryption, resetOperationEncryption } from './encryption';
13
+
14
+ describe('EncryptedOfflineQueue', () => {
15
+ let queue: EncryptedOfflineQueue;
16
+
17
+ beforeEach(async () => {
18
+ resetOfflineQueue();
19
+ resetOperationEncryption();
20
+ queue = new EncryptedOfflineQueue();
21
+ await queue.initialize();
22
+ });
23
+
24
+ afterEach(() => {
25
+ queue.shutdown();
26
+ });
27
+
28
+ describe('initialization', () => {
29
+ test('initializes successfully', async () => {
30
+ const newQueue = new EncryptedOfflineQueue();
31
+ await newQueue.initialize();
32
+
33
+ const stats = newQueue.getStats();
34
+ expect(stats.total).toBe(0);
35
+ expect(stats.pending).toBe(0);
36
+
37
+ newQueue.shutdown();
38
+ });
39
+
40
+ test('emits initialized event', async () => {
41
+ const newQueue = new EncryptedOfflineQueue();
42
+ let initialized = false;
43
+
44
+ newQueue.on('initialized', () => {
45
+ initialized = true;
46
+ });
47
+
48
+ await newQueue.initialize();
49
+ expect(initialized).toBe(true);
50
+
51
+ newQueue.shutdown();
52
+ });
53
+
54
+ test('only initializes once', async () => {
55
+ const newQueue = new EncryptedOfflineQueue();
56
+ await newQueue.initialize();
57
+ await newQueue.initialize(); // Second call should be no-op
58
+
59
+ const stats = newQueue.getStats();
60
+ expect(stats.total).toBe(0);
61
+
62
+ newQueue.shutdown();
63
+ });
64
+ });
65
+
66
+ describe('queueOperation', () => {
67
+ test('queues an operation and returns ID', async () => {
68
+ const operationId = await queue.queueOperation({
69
+ type: 'create',
70
+ sessionId: 'session-123',
71
+ data: { name: 'test' },
72
+ priority: 'normal',
73
+ createdAt: Date.now(),
74
+ });
75
+
76
+ expect(operationId).toBeDefined();
77
+ expect(operationId.startsWith('op_')).toBe(true);
78
+ });
79
+
80
+ test('increments queue stats', async () => {
81
+ await queue.queueOperation({
82
+ type: 'create',
83
+ sessionId: 'session-123',
84
+ data: { name: 'test' },
85
+ priority: 'normal',
86
+ createdAt: Date.now(),
87
+ });
88
+
89
+ const stats = queue.getStats();
90
+ expect(stats.total).toBe(1);
91
+ expect(stats.pending).toBe(1);
92
+ });
93
+
94
+ test('emits operation:queued event', async () => {
95
+ let eventData: {
96
+ operationId: string;
97
+ sessionId: string;
98
+ size: number;
99
+ } | null = null;
100
+
101
+ queue.on('operation:queued', (data) => {
102
+ eventData = data as {
103
+ operationId: string;
104
+ sessionId: string;
105
+ size: number;
106
+ };
107
+ });
108
+
109
+ await queue.queueOperation({
110
+ type: 'create',
111
+ sessionId: 'session-123',
112
+ data: { name: 'test' },
113
+ priority: 'normal',
114
+ createdAt: Date.now(),
115
+ });
116
+
117
+ expect(eventData).not.toBeNull();
118
+ expect(eventData!.sessionId).toBe('session-123');
119
+ expect(eventData!.size).toBeGreaterThan(0);
120
+ });
121
+
122
+ test('throws if queue not initialized', async () => {
123
+ const uninitializedQueue = new EncryptedOfflineQueue();
124
+
125
+ await expect(
126
+ uninitializedQueue.queueOperation({
127
+ type: 'create',
128
+ sessionId: 'session-123',
129
+ data: {},
130
+ priority: 'normal',
131
+ createdAt: Date.now(),
132
+ }),
133
+ ).rejects.toThrow('Queue not initialized');
134
+ });
135
+
136
+ test('tracks bytes used', async () => {
137
+ await queue.queueOperation({
138
+ type: 'create',
139
+ sessionId: 'session-123',
140
+ data: { largeData: 'x'.repeat(1000) },
141
+ priority: 'normal',
142
+ createdAt: Date.now(),
143
+ });
144
+
145
+ const stats = queue.getStats();
146
+ expect(stats.totalBytes).toBeGreaterThan(1000);
147
+ });
148
+ });
149
+
150
+ describe('getPendingOperations', () => {
151
+ test('returns pending operations', async () => {
152
+ await queue.queueOperation({
153
+ type: 'create',
154
+ sessionId: 'session-123',
155
+ data: { name: 'test1' },
156
+ priority: 'normal',
157
+ createdAt: Date.now(),
158
+ });
159
+
160
+ await queue.queueOperation({
161
+ type: 'update',
162
+ sessionId: 'session-123',
163
+ data: { name: 'test2' },
164
+ priority: 'normal',
165
+ createdAt: Date.now(),
166
+ });
167
+
168
+ const pending = queue.getPendingOperations();
169
+ expect(pending).toHaveLength(2);
170
+ });
171
+
172
+ test('filters by sessionId', async () => {
173
+ await queue.queueOperation({
174
+ type: 'create',
175
+ sessionId: 'session-1',
176
+ data: {},
177
+ priority: 'normal',
178
+ createdAt: Date.now(),
179
+ });
180
+
181
+ await queue.queueOperation({
182
+ type: 'create',
183
+ sessionId: 'session-2',
184
+ data: {},
185
+ priority: 'normal',
186
+ createdAt: Date.now(),
187
+ });
188
+
189
+ const pending = queue.getPendingOperations('session-1');
190
+ expect(pending).toHaveLength(1);
191
+ expect(pending[0].sessionId).toBe('session-1');
192
+ });
193
+
194
+ test('respects limit parameter', async () => {
195
+ for (let i = 0; i < 10; i++) {
196
+ await queue.queueOperation({
197
+ type: 'create',
198
+ sessionId: 'session-123',
199
+ data: { index: i },
200
+ priority: 'normal',
201
+ createdAt: Date.now(),
202
+ });
203
+ }
204
+
205
+ const pending = queue.getPendingOperations(undefined, 5);
206
+ expect(pending).toHaveLength(5);
207
+ });
208
+
209
+ test('sorts by priority then createdAt', async () => {
210
+ const now = Date.now();
211
+
212
+ await queue.queueOperation({
213
+ type: 'create',
214
+ sessionId: 'session-123',
215
+ data: { name: 'low' },
216
+ priority: 'low',
217
+ createdAt: now,
218
+ });
219
+
220
+ await queue.queueOperation({
221
+ type: 'create',
222
+ sessionId: 'session-123',
223
+ data: { name: 'high' },
224
+ priority: 'high',
225
+ createdAt: now + 100,
226
+ });
227
+
228
+ await queue.queueOperation({
229
+ type: 'create',
230
+ sessionId: 'session-123',
231
+ data: { name: 'normal' },
232
+ priority: 'normal',
233
+ createdAt: now + 50,
234
+ });
235
+
236
+ const pending = queue.getPendingOperations();
237
+ expect(pending[0].priority).toBe('high');
238
+ expect(pending[1].priority).toBe('normal');
239
+ expect(pending[2].priority).toBe('low');
240
+ });
241
+ });
242
+
243
+ describe('markSyncing', () => {
244
+ test('changes operation status to syncing', async () => {
245
+ const operationId = await queue.queueOperation({
246
+ type: 'create',
247
+ sessionId: 'session-123',
248
+ data: {},
249
+ priority: 'normal',
250
+ createdAt: Date.now(),
251
+ });
252
+
253
+ queue.markSyncing(operationId);
254
+
255
+ const stats = queue.getStats();
256
+ expect(stats.pending).toBe(0);
257
+ expect(stats.syncing).toBe(1);
258
+ });
259
+
260
+ test('emits operation:syncing event', async () => {
261
+ let eventOperationId: string | null = null;
262
+
263
+ queue.on('operation:syncing', (data) => {
264
+ eventOperationId = (data as { operationId: string }).operationId;
265
+ });
266
+
267
+ const operationId = await queue.queueOperation({
268
+ type: 'create',
269
+ sessionId: 'session-123',
270
+ data: {},
271
+ priority: 'normal',
272
+ createdAt: Date.now(),
273
+ });
274
+
275
+ queue.markSyncing(operationId);
276
+
277
+ expect(eventOperationId).toBe(operationId);
278
+ });
279
+ });
280
+
281
+ describe('markSynced', () => {
282
+ test('changes operation status to synced', async () => {
283
+ const operationId = await queue.queueOperation({
284
+ type: 'create',
285
+ sessionId: 'session-123',
286
+ data: {},
287
+ priority: 'normal',
288
+ createdAt: Date.now(),
289
+ });
290
+
291
+ queue.markSyncing(operationId);
292
+ queue.markSynced(operationId);
293
+
294
+ const stats = queue.getStats();
295
+ expect(stats.syncing).toBe(0);
296
+ expect(stats.synced).toBe(1);
297
+ });
298
+
299
+ test('emits operation:synced event', async () => {
300
+ let eventOperationId: string | null = null;
301
+
302
+ queue.on('operation:synced', (data) => {
303
+ eventOperationId = (data as { operationId: string }).operationId;
304
+ });
305
+
306
+ const operationId = await queue.queueOperation({
307
+ type: 'create',
308
+ sessionId: 'session-123',
309
+ data: {},
310
+ priority: 'normal',
311
+ createdAt: Date.now(),
312
+ });
313
+
314
+ queue.markSynced(operationId);
315
+
316
+ expect(eventOperationId).toBe(operationId);
317
+ });
318
+ });
319
+
320
+ describe('markFailed', () => {
321
+ test('increments retry count and keeps pending', async () => {
322
+ const operationId = await queue.queueOperation({
323
+ type: 'create',
324
+ sessionId: 'session-123',
325
+ data: {},
326
+ priority: 'normal',
327
+ createdAt: Date.now(),
328
+ });
329
+
330
+ queue.markFailed(operationId, 'Network error');
331
+
332
+ const stats = queue.getStats();
333
+ expect(stats.pending).toBe(1);
334
+ expect(stats.failed).toBe(0);
335
+ });
336
+
337
+ test('emits operation:retry event', async () => {
338
+ let retryAttempt = 0;
339
+
340
+ queue.on('operation:retry', (data) => {
341
+ retryAttempt = (data as { attempt: number }).attempt;
342
+ });
343
+
344
+ const operationId = await queue.queueOperation({
345
+ type: 'create',
346
+ sessionId: 'session-123',
347
+ data: {},
348
+ priority: 'normal',
349
+ createdAt: Date.now(),
350
+ });
351
+
352
+ queue.markFailed(operationId, 'Network error');
353
+
354
+ expect(retryAttempt).toBe(1);
355
+ });
356
+
357
+ test('marks as failed after max retries', async () => {
358
+ let failedEventReceived = false;
359
+
360
+ queue.on('operation:failed_max_retries', () => {
361
+ failedEventReceived = true;
362
+ });
363
+
364
+ const operationId = await queue.queueOperation({
365
+ type: 'create',
366
+ sessionId: 'session-123',
367
+ data: {},
368
+ priority: 'normal',
369
+ createdAt: Date.now(),
370
+ });
371
+
372
+ // Fail 5 times (max retries)
373
+ for (let i = 0; i < 5; i++) {
374
+ queue.markFailed(operationId, 'Network error');
375
+ }
376
+
377
+ expect(failedEventReceived).toBe(true);
378
+
379
+ const stats = queue.getStats();
380
+ expect(stats.failed).toBe(1);
381
+ expect(stats.pending).toBe(0);
382
+ });
383
+ });
384
+
385
+ describe('removeOperation', () => {
386
+ test('removes operation from queue', async () => {
387
+ const operationId = await queue.queueOperation({
388
+ type: 'create',
389
+ sessionId: 'session-123',
390
+ data: {},
391
+ priority: 'normal',
392
+ createdAt: Date.now(),
393
+ });
394
+
395
+ const removed = queue.removeOperation(operationId);
396
+
397
+ expect(removed).toBe(true);
398
+
399
+ const stats = queue.getStats();
400
+ expect(stats.total).toBe(0);
401
+ });
402
+
403
+ test('returns false for non-existent operation', () => {
404
+ const removed = queue.removeOperation('non-existent-id');
405
+ expect(removed).toBe(false);
406
+ });
407
+
408
+ test('decreases bytes used', async () => {
409
+ const operationId = await queue.queueOperation({
410
+ type: 'create',
411
+ sessionId: 'session-123',
412
+ data: { largeData: 'x'.repeat(1000) },
413
+ priority: 'normal',
414
+ createdAt: Date.now(),
415
+ });
416
+
417
+ const statsBefore = queue.getStats();
418
+ queue.removeOperation(operationId);
419
+ const statsAfter = queue.getStats();
420
+
421
+ expect(statsAfter.totalBytes).toBeLessThan(statsBefore.totalBytes);
422
+ });
423
+ });
424
+
425
+ describe('getDecryptedOperation', () => {
426
+ test('returns operation data without encryption', async () => {
427
+ const operationId = await queue.queueOperation({
428
+ type: 'create',
429
+ sessionId: 'session-123',
430
+ data: { name: 'test' },
431
+ priority: 'high',
432
+ createdAt: Date.now(),
433
+ });
434
+
435
+ const decrypted = await queue.getDecryptedOperation(operationId);
436
+
437
+ expect(decrypted).not.toBeNull();
438
+ expect(decrypted!.type).toBe('create');
439
+ expect(decrypted!.sessionId).toBe('session-123');
440
+ expect(decrypted!.data).toEqual({ name: 'test' });
441
+ expect(decrypted!.priority).toBe('high');
442
+ });
443
+
444
+ test('returns null for non-existent operation', async () => {
445
+ const decrypted = await queue.getDecryptedOperation('non-existent-id');
446
+ expect(decrypted).toBeNull();
447
+ });
448
+ });
449
+
450
+ describe('getStats', () => {
451
+ test('returns correct statistics', async () => {
452
+ await queue.queueOperation({
453
+ type: 'create',
454
+ sessionId: 'session-123',
455
+ data: {},
456
+ priority: 'normal',
457
+ createdAt: Date.now(),
458
+ });
459
+
460
+ const op2 = await queue.queueOperation({
461
+ type: 'update',
462
+ sessionId: 'session-123',
463
+ data: {},
464
+ priority: 'normal',
465
+ createdAt: Date.now(),
466
+ });
467
+
468
+ queue.markSyncing(op2);
469
+
470
+ const stats = queue.getStats();
471
+ expect(stats.total).toBe(2);
472
+ expect(stats.pending).toBe(1);
473
+ expect(stats.syncing).toBe(1);
474
+ expect(stats.synced).toBe(0);
475
+ expect(stats.failed).toBe(0);
476
+ });
477
+
478
+ test('indicates compaction needed when threshold exceeded', async () => {
479
+ // Create a queue with very small capacity
480
+ const smallQueue = createOfflineQueue({
481
+ maxLocalCapacity: 1000,
482
+ compactionThreshold: 0.5,
483
+ });
484
+ await smallQueue.initialize();
485
+
486
+ // Add operations to exceed threshold
487
+ await smallQueue.queueOperation({
488
+ type: 'create',
489
+ sessionId: 'session-123',
490
+ data: { largeData: 'x'.repeat(600) },
491
+ priority: 'normal',
492
+ createdAt: Date.now(),
493
+ });
494
+
495
+ const stats = smallQueue.getStats();
496
+ expect(stats.compactionNeeded).toBe(true);
497
+
498
+ smallQueue.shutdown();
499
+ });
500
+ });
501
+
502
+ describe('clear', () => {
503
+ test('clears all operations', async () => {
504
+ await queue.queueOperation({
505
+ type: 'create',
506
+ sessionId: 'session-123',
507
+ data: {},
508
+ priority: 'normal',
509
+ createdAt: Date.now(),
510
+ });
511
+
512
+ await queue.queueOperation({
513
+ type: 'update',
514
+ sessionId: 'session-123',
515
+ data: {},
516
+ priority: 'normal',
517
+ createdAt: Date.now(),
518
+ });
519
+
520
+ queue.clear();
521
+
522
+ const stats = queue.getStats();
523
+ expect(stats.total).toBe(0);
524
+ expect(stats.totalBytes).toBe(0);
525
+ });
526
+ });
527
+
528
+ describe('shutdown', () => {
529
+ test('emits shutdown event', async () => {
530
+ let shutdownReceived = false;
531
+
532
+ queue.on('shutdown', () => {
533
+ shutdownReceived = true;
534
+ });
535
+
536
+ queue.shutdown();
537
+
538
+ expect(shutdownReceived).toBe(true);
539
+ });
540
+ });
541
+
542
+ describe('with encryption enabled', () => {
543
+ test('encrypts operations when key material provided', async () => {
544
+ const encryptedQueue = createOfflineQueue({
545
+ encryption: {
546
+ enabled: true,
547
+ keyDerivation: 'session',
548
+ },
549
+ });
550
+
551
+ const encryption = getOperationEncryption();
552
+ const keyMaterial = await encryption.deriveKeyFromSession(
553
+ 'session-123',
554
+ 'queue-test',
555
+ );
556
+
557
+ await encryptedQueue.initialize({ keyMaterial });
558
+
559
+ const operationId = await encryptedQueue.queueOperation({
560
+ type: 'create',
561
+ sessionId: 'session-123',
562
+ data: { secret: 'data' },
563
+ priority: 'normal',
564
+ createdAt: Date.now(),
565
+ });
566
+
567
+ // Operation should have encrypted data
568
+ const pending = encryptedQueue.getPendingOperations();
569
+ expect(pending[0].encryptedData).toBeInstanceOf(Uint8Array);
570
+
571
+ // Should still be decryptable
572
+ const decrypted = await encryptedQueue.getDecryptedOperation(operationId);
573
+ expect(decrypted!.data).toEqual({ secret: 'data' });
574
+
575
+ encryptedQueue.shutdown();
576
+ });
577
+ });
578
+ });
579
+
580
+ describe('getOfflineQueue', () => {
581
+ beforeEach(() => {
582
+ resetOfflineQueue();
583
+ });
584
+
585
+ test('returns singleton instance', () => {
586
+ const instance1 = getOfflineQueue();
587
+ const instance2 = getOfflineQueue();
588
+
589
+ expect(instance1).toBe(instance2);
590
+ });
591
+ });
592
+
593
+ describe('createOfflineQueue', () => {
594
+ test('creates queue with custom config', async () => {
595
+ const customQueue = createOfflineQueue({
596
+ maxLocalCapacity: 10 * 1024 * 1024, // 10MB
597
+ compactionThreshold: 0.9,
598
+ });
599
+
600
+ await customQueue.initialize();
601
+
602
+ const stats = customQueue.getStats();
603
+ expect(stats.total).toBe(0);
604
+
605
+ customQueue.shutdown();
606
+ });
607
+ });