📄 src/web3/subscription/patch-resolver.test.ts
D-OPEN SOVEREIGN
1import { describe, it, expect, vi } from 'vitest';
2import { fetchPatches } from './patch-resolver';
3import type { SubscriptionGraph } from './types';
4
5function graphWith(userIds: number[]): SubscriptionGraph {
6  const nodes = new Map(userIds.map((id) => [id, { userId: id, subscriptions: [] }]));
7  return { rootUserId: userIds[0], nodes, truncated: false };
8}
9
10function fakeClient(loadActionsSince: (userId: number, fromIndex: number) => Promise<any>) {
11  return { archivistStorage: { loadActionsSince } } as any;
12}
13
14describe('fetchPatches', () => {
15  it('returns an empty array for an empty graph', async () => {
16    const client = fakeClient(async () => [[], [], []]);
17    const patches = await fetchPatches(client, graphWith([]));
18    expect(patches).toEqual([]);
19  });
20
21  it('skips nodes with no new actions', async () => {
22    const client = fakeClient(async () => [[], [], []]);
23    const patches = await fetchPatches(client, graphWith([1, 2]));
24    expect(patches).toEqual([]);
25  });
26
27  it('fetches and shapes patches for nodes with new actions', async () => {
28    const client = fakeClient(async (userId) => [[BigInt(userId)], [1n], ['x']]);
29    const patches = await fetchPatches(client, graphWith([1, 2]));
30
31    expect(patches).toHaveLength(2);
32    const byUser = new Map(patches.map((p) => [p.userId, p]));
33    expect(byUser.get(1)).toEqual({
34      userId: 1,
35      fromIndex: 0,
36      toIndex: 1,
37      raw: { compressedActions: [1n], indexes: [1n], stringIndexes: ['x'] },
38    });
39  });
40
41  it('computes toIndex from fromIndex plus the number of returned actions', async () => {
42    const client = fakeClient(async () => [[1n, 2n, 3n], [1n, 2n, 3n], []]);
43    const [patch] = await fetchPatches(client, graphWith([1]), new Map([[1, 10]]));
44    expect(patch.fromIndex).toBe(10);
45    expect(patch.toIndex).toBe(13);
46  });
47
48  it('defaults fromIndex to 0 for a node missing from the cursor map', async () => {
49    const seen: number[] = [];
50    const client = fakeClient(async (_userId, fromIndex) => {
51      seen.push(fromIndex);
52      return [[], [], []];
53    });
54    await fetchPatches(client, graphWith([1, 2]), new Map([[1, 5]]));
55    expect(seen.sort()).toEqual([0, 5]);
56  });
57
58  it('continues fetching other nodes when one node errors', async () => {
59    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
60    const client = fakeClient(async (userId) => {
61      if (userId === 1) throw new Error('rpc down');
62      return [[9n], [1n], []];
63    });
64
65    const patches = await fetchPatches(client, graphWith([1, 2]));
66
67    expect(patches).toHaveLength(1);
68    expect(patches[0].userId).toBe(2);
69    warnSpy.mockRestore();
70  });
71
72  it('respects the concurrency option by never issuing more than N in-flight calls at once', async () => {
73    let inFlight = 0;
74    let maxInFlight = 0;
75    const client = fakeClient(async () => {
76      inFlight++;
77      maxInFlight = Math.max(maxInFlight, inFlight);
78      await new Promise((r) => setTimeout(r, 5));
79      inFlight--;
80      return [[], [], []];
81    });
82
83    await fetchPatches(client, graphWith([1, 2, 3, 4, 5, 6, 7]), undefined, { concurrency: 2 });
84
85    expect(maxInFlight).toBeLessThanOrEqual(2);
86  });
87
88  it('defaults concurrency to 4 in-flight calls when not specified', async () => {
89    let inFlight = 0;
90    let maxInFlight = 0;
91    const client = fakeClient(async () => {
92      inFlight++;
93      maxInFlight = Math.max(maxInFlight, inFlight);
94      await new Promise((r) => setTimeout(r, 5));
95      inFlight--;
96      return [[], [], []];
97    });
98
99    await fetchPatches(client, graphWith([1, 2, 3, 4, 5, 6, 7, 8, 9]));
100
101    expect(maxInFlight).toBe(4);
102  });
103});
104