📄 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