📄 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