This repository has no description
1import { describe, expect, it, vi } from 'vitest';
2import { ClientResponseError } from './client';
3import { createRequestCache, httpStatusFor, parallel, toHttpError } from './load';
4
5const cre = (status: number, error: string, message?: string): ClientResponseError =>
6 new ClientResponseError({ status, data: { error, message } });
7
8describe('httpStatusFor', () => {
9 it('passes through an in-band XRPC status', () => {
10 expect(httpStatusFor(cre(404, 'RecordNotFound'))).toBe(404);
11 });
12
13 it('prefers an in-band status over the error-name mapping', () => {
14 // status 503 is in [400,599] so it wins even though the name maps to 404.
15 expect(httpStatusFor(cre(503, 'RecordNotFound'))).toBe(503);
16 });
17
18 it('maps the error name when the status is out of the 400-599 band', () => {
19 expect(httpStatusFor(cre(200, 'RecordNotFound'))).toBe(404);
20 expect(httpStatusFor(cre(200, 'InvalidRequest'))).toBe(400);
21 expect(httpStatusFor(cre(200, 'UpstreamFailed'))).toBe(502);
22 expect(httpStatusFor(cre(200, 'UpstreamGone'))).toBe(502);
23 expect(httpStatusFor(cre(200, 'InvalidRecord'))).toBe(502);
24 expect(httpStatusFor(cre(200, 'Overloaded'))).toBe(503);
25 expect(httpStatusFor(cre(200, 'SomethingElse'))).toBe(500);
26 });
27
28 it('maps any non-XRPC cause to 500', () => {
29 expect(httpStatusFor(new Error('boom'))).toBe(500);
30 expect(httpStatusFor('a string')).toBe(500);
31 expect(httpStatusFor(undefined)).toBe(500);
32 expect(httpStatusFor({ status: 404 })).toBe(500);
33 });
34});
35
36describe('toHttpError', () => {
37 it('throws a SvelteKit error with the mapped status and the XRPC description', () => {
38 let thrown: unknown;
39 try {
40 toHttpError(cre(404, 'RecordNotFound', 'no such repo'));
41 } catch (e) {
42 thrown = e;
43 }
44 expect(thrown).toMatchObject({ status: 404, body: { message: 'no such repo' } });
45 });
46
47 it('falls back to the error name when the description is absent', () => {
48 let thrown: unknown;
49 try {
50 toHttpError(cre(200, 'Overloaded'));
51 } catch (e) {
52 thrown = e;
53 }
54 expect(thrown).toMatchObject({ status: 503, body: { message: 'Overloaded' } });
55 });
56
57 it('uses the fallback message + 500 for a non-XRPC cause', () => {
58 let thrown: unknown;
59 try {
60 toHttpError(new Error('boom'), 'could not load');
61 } catch (e) {
62 thrown = e;
63 }
64 expect(thrown).toMatchObject({ status: 500, body: { message: 'could not load' } });
65 });
66});
67
68describe('parallel', () => {
69 it('preserves key/value pairing regardless of settle order', async () => {
70 // deferred across a few microtasks (no real timer) so it settles after `fast`.
71 const slow = Promise.resolve()
72 .then(() => Promise.resolve())
73 .then(() => 'slow');
74 const result = await parallel({ fast: Promise.resolve('fast'), slow });
75 expect(result).toEqual({ fast: 'fast', slow: 'slow' });
76 });
77
78 it('resolves to an empty object for no tasks', async () => {
79 await expect(parallel({})).resolves.toEqual({});
80 });
81
82 it('rejects if any input promise rejects', async () => {
83 await expect(
84 parallel({ a: Promise.resolve(1), b: Promise.reject(new Error('boom')) })
85 ).rejects.toThrow('boom');
86 });
87});
88
89describe('createRequestCache', () => {
90 it('invokes the loader once per key and shares the value', async () => {
91 const cache = createRequestCache();
92 const load = vi.fn(async () => 42);
93
94 const first = await cache.run('k', load);
95 const second = await cache.run('k', load);
96
97 expect(first).toBe(42);
98 expect(second).toBe(42);
99 expect(load).toHaveBeenCalledTimes(1);
100 });
101
102 it('shares one in-flight promise for concurrent same-key calls', () => {
103 const cache = createRequestCache();
104 const load = vi.fn(async () => 'v');
105
106 const a = cache.run('k', load);
107 const b = cache.run('k', load);
108
109 expect(a).toBe(b);
110 expect(load).toHaveBeenCalledTimes(1);
111 });
112
113 it('runs distinct keys independently', async () => {
114 const cache = createRequestCache();
115 const load = vi.fn(async (key: string) => key.toUpperCase());
116
117 await cache.run('a', () => load('a'));
118 await cache.run('b', () => load('b'));
119
120 expect(load).toHaveBeenCalledTimes(2);
121 });
122});