📄 src/web3/subscription/sync-cursor.test.ts
D-OPEN SOVEREIGN
1import { describe, it, expect, vi, beforeEach } from 'vitest';
2import { serializeCursor, deserializeCursor, emptyCursor, cursorStore } from './sync-cursor';
3import type { SyncCursor } from './types';
4
5const { get, set, del } = vi.hoisted(() => ({ get: vi.fn(), set: vi.fn(), del: vi.fn() }));
6vi.mock('idb-keyval', () => ({ get, set, del }));
7
8beforeEach(() => {
9 get.mockReset();
10 set.mockReset();
11 del.mockReset();
12});
13
14function sampleCursor(): SyncCursor {
15 return {
16 rootUserId: 1,
17 knownNodes: new Map([[1, { userId: 1, subscriptions: [2, 3] }]]),
18 countSnapshot: new Map([[1, 5]]),
19 patchIndex: new Map([[1, 10], [2, 0]]),
20 };
21}
22
23describe('emptyCursor', () => {
24 it('creates a cursor with empty maps for the given root', () => {
25 const cursor = emptyCursor(42);
26 expect(cursor.rootUserId).toBe(42);
27 expect(cursor.knownNodes.size).toBe(0);
28 expect(cursor.countSnapshot.size).toBe(0);
29 expect(cursor.patchIndex.size).toBe(0);
30 });
31});
32
33describe('serializeCursor / deserializeCursor', () => {
34 it('round-trips a cursor through plain-array serialization', () => {
35 const cursor = sampleCursor();
36 const roundTripped = deserializeCursor(serializeCursor(cursor));
37
38 expect(roundTripped.rootUserId).toBe(cursor.rootUserId);
39 expect(roundTripped.knownNodes).toEqual(cursor.knownNodes);
40 expect(roundTripped.countSnapshot).toEqual(cursor.countSnapshot);
41 expect(roundTripped.patchIndex).toEqual(cursor.patchIndex);
42 });
43
44 it('produces JSON-serializable plain arrays (no Map instances)', () => {
45 const serialized = serializeCursor(sampleCursor());
46 expect(() => JSON.stringify(serialized)).not.toThrow();
47 expect(Array.isArray(serialized.knownNodes)).toBe(true);
48 });
49
50 it('round-trips an empty cursor', () => {
51 const cursor = emptyCursor(7);
52 const roundTripped = deserializeCursor(serializeCursor(cursor));
53 expect(roundTripped.knownNodes.size).toBe(0);
54 });
55});
56
57describe('cursorStore', () => {
58 it('get() returns undefined when nothing is stored', async () => {
59 get.mockResolvedValue(undefined);
60 const result = await cursorStore.get(1);
61 expect(result).toBeUndefined();
62 expect(get).toHaveBeenCalledWith('web3-sdk:subscription-cursor:1');
63 });
64
65 it('get() deserializes a stored cursor', async () => {
66 get.mockResolvedValue(serializeCursor(sampleCursor()));
67 const result = await cursorStore.get(1);
68 expect(result?.knownNodes.get(1)).toEqual({ userId: 1, subscriptions: [2, 3] });
69 });
70
71 it('get() swallows storage errors and returns undefined', async () => {
72 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
73 get.mockRejectedValue(new Error('idb unavailable'));
74 const result = await cursorStore.get(1);
75 expect(result).toBeUndefined();
76 warnSpy.mockRestore();
77 });
78
79 it('set() persists the serialized cursor under a userId-scoped key', async () => {
80 const cursor = sampleCursor();
81 await cursorStore.set(9, cursor);
82 expect(set).toHaveBeenCalledWith('web3-sdk:subscription-cursor:9', serializeCursor(cursor));
83 });
84
85 it('set() swallows storage errors instead of throwing', async () => {
86 const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
87 set.mockRejectedValue(new Error('quota exceeded'));
88 await expect(cursorStore.set(1, emptyCursor(1))).resolves.toBeUndefined();
89 warnSpy.mockRestore();
90 });
91
92 it('clear() deletes the userId-scoped key', async () => {
93 await cursorStore.clear(3);
94 expect(del).toHaveBeenCalledWith('web3-sdk:subscription-cursor:3');
95 });
96
97 it('clear() swallows storage errors instead of throwing', async () => {
98 del.mockRejectedValue(new Error('boom'));
99 await expect(cursorStore.clear(3)).resolves.toBeUndefined();
100 });
101});
102