144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { createOfflineQueue } from './index.js';
|
|
|
|
function createMemoryStorage() {
|
|
const store: Record<string, string> = {};
|
|
return {
|
|
getItem: (k: string) => store[k] ?? null,
|
|
setItem: (k: string, v: string) => {
|
|
store[k] = v;
|
|
},
|
|
_store: store,
|
|
};
|
|
}
|
|
|
|
describe('createOfflineQueue', () => {
|
|
it('should start with zero length', () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage });
|
|
expect(queue.length()).toBe(0);
|
|
});
|
|
|
|
it('should enqueue items', () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage });
|
|
|
|
queue.enqueue({ id: '1', action: 'create', path: '/sessions', payload: { name: 'test' } });
|
|
expect(queue.length()).toBe(1);
|
|
|
|
queue.enqueue({ id: '2', action: 'update', path: '/sessions/2', payload: { name: 'test2' } });
|
|
expect(queue.length()).toBe(2);
|
|
});
|
|
|
|
it('should replace existing entry with same id + action', () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage });
|
|
|
|
queue.enqueue({ id: '1', action: 'create', path: '/sessions', payload: { v: 1 } });
|
|
queue.enqueue({ id: '1', action: 'create', path: '/sessions', payload: { v: 2 } });
|
|
expect(queue.length()).toBe(1);
|
|
});
|
|
|
|
it('should not replace entries with different action', () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage });
|
|
|
|
queue.enqueue({ id: '1', action: 'create', path: '/sessions', payload: {} });
|
|
queue.enqueue({ id: '1', action: 'update', path: '/sessions/1', payload: {} });
|
|
expect(queue.length()).toBe(2);
|
|
});
|
|
|
|
it('should flush all items successfully', async () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage });
|
|
|
|
queue.enqueue({ id: '1', action: 'create', path: '/a', payload: { n: 1 } });
|
|
queue.enqueue({ id: '2', action: 'create', path: '/b', payload: { n: 2 } });
|
|
|
|
const executor = vi.fn().mockResolvedValue(undefined);
|
|
const result = await queue.flush(executor);
|
|
|
|
expect(result.flushed).toBe(2);
|
|
expect(result.failed).toBe(0);
|
|
expect(queue.length()).toBe(0);
|
|
expect(executor).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should retry failed items on next flush', async () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage, maxRetries: 3 });
|
|
|
|
queue.enqueue({ id: '1', action: 'create', path: '/a', payload: {} });
|
|
|
|
const failingExecutor = vi.fn().mockRejectedValue(new Error('offline'));
|
|
const result = await queue.flush(failingExecutor);
|
|
|
|
expect(result.flushed).toBe(0);
|
|
expect(result.failed).toBe(1);
|
|
expect(queue.length()).toBe(1);
|
|
});
|
|
|
|
it('should drop items after max retries', async () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage, maxRetries: 2 });
|
|
|
|
queue.enqueue({ id: '1', action: 'create', path: '/a', payload: {} });
|
|
|
|
const failingExecutor = vi.fn().mockRejectedValue(new Error('offline'));
|
|
|
|
// Retry 1
|
|
await queue.flush(failingExecutor);
|
|
expect(queue.length()).toBe(1);
|
|
|
|
// Retry 2 — should be dropped
|
|
await queue.flush(failingExecutor);
|
|
expect(queue.length()).toBe(0);
|
|
});
|
|
|
|
it('should cap queue at maxQueueSize', () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage, maxQueueSize: 3 });
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
queue.enqueue({ id: `${i}`, action: 'create', path: `/item/${i}`, payload: {} });
|
|
}
|
|
|
|
expect(queue.length()).toBe(3);
|
|
});
|
|
|
|
it('should persist to storage', () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage });
|
|
|
|
queue.enqueue({ id: '1', action: 'create', path: '/a', payload: { x: 1 } });
|
|
|
|
const stored = JSON.parse(storage._store['test-q']);
|
|
expect(stored).toHaveLength(1);
|
|
expect(stored[0].id).toBe('1');
|
|
});
|
|
|
|
it('should clear the queue', () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage });
|
|
|
|
queue.enqueue({ id: '1', action: 'create', path: '/a', payload: {} });
|
|
queue.enqueue({ id: '2', action: 'create', path: '/b', payload: {} });
|
|
expect(queue.length()).toBe(2);
|
|
|
|
queue.clear();
|
|
expect(queue.length()).toBe(0);
|
|
});
|
|
|
|
it('should return empty result when flushing empty queue', async () => {
|
|
const storage = createMemoryStorage();
|
|
const queue = createOfflineQueue({ storageKey: 'test-q', storage });
|
|
|
|
const executor = vi.fn();
|
|
const result = await queue.flush(executor);
|
|
|
|
expect(result.flushed).toBe(0);
|
|
expect(result.failed).toBe(0);
|
|
expect(executor).not.toHaveBeenCalled();
|
|
});
|
|
});
|