import { Tidx as TidxClient } from 'tidx.ts' import * as Zones from '../apps/Zones.js' import type * as Auth from './Auth.js' import * as Tidx from './Tidx.js' import type * as Viem from './Viem.js' /** Anonymous (public) request principal. */ const publicPrincipal: Auth.Principal = { id: 'pub', type: 'public' } /** Authenticated API-key request principal. */ const apiKeyPrincipal: Auth.Principal = { apiKey: {} as never, environment: 'production', id: 'key_test', orgId: 'org_test', type: 'api_key', } /** Builds a `FetchRequestError` as the indexer surfaces it at a given status. */ function appError(message: string, status = 200) { return new TidxClient.FetchRequestError(message, new Response(null, { status })) } /** Minimal client whose `fetch` is a controllable mock. */ function fakeClient(fetch: Tidx.Client['fetch']): Tidx.Client { return { fetch } as never } describe('providerFailure', () => { test('classifies failed 200 bodies as payload failures', () => { expect(Tidx.providerFailure(appError('secret upstream detail'))).toEqual({ failure: 'payload', id: 'tidx', operation: 'query', }) }) test('classifies query and HTTP failures', () => { expect([ Tidx.providerFailure(appError('secret upstream detail', 422)), Tidx.providerFailure(appError('secret upstream detail', 503)), ]).toMatchInlineSnapshot(` [ { "failure": "query", "id": "tidx", "operation": "query", "status": 422, }, { "failure": "http", "id": "tidx", "operation": "query", "status": 503, }, ] `) }) test('classifies timeout and rate-limit statuses', () => { expect([ Tidx.providerFailure(appError('query timed out')), Tidx.providerFailure(appError('secret upstream detail', 408)), Tidx.providerFailure(appError('secret upstream detail', 429)), ]).toMatchInlineSnapshot(` [ { "failure": "timeout", "id": "tidx", "operation": "query", }, { "failure": "timeout", "id": "tidx", "operation": "query", "status": 408, }, { "failure": "rate_limit", "id": "tidx", "operation": "query", "status": 429, }, ] `) }) test('classifies transport failures without their messages', () => { expect([ Tidx.providerFailure(new DOMException('secret timeout detail', 'TimeoutError')), Tidx.providerFailure(new TypeError('secret network detail')), Tidx.providerFailure(new Error('secret unknown detail')), ]).toMatchInlineSnapshot(` [ { "failure": "timeout", "id": "tidx", "operation": "query", }, { "failure": "network", "id": "tidx", "operation": "query", }, { "failure": "unknown", "id": "tidx", "operation": "query", }, ] `) }) }) describe('responseFailure', () => { test('classifies failed 200 bodies without consuming the response', async () => { const response = Response.json({ error: 'secret upstream detail', ok: false }) expect(await Tidx.responseFailure(response)).toEqual({ failure: 'payload', id: 'tidx', operation: 'query', }) expect(await response.json()).toEqual({ error: 'secret upstream detail', ok: false }) }) test('ignores successful 200 bodies', async () => { const response = Response.json({ ok: true }) expect(await Tidx.responseFailure(response)).toBeUndefined() }) }) describe('isQueryRejection', () => { test('matches caller-caused query statuses only', () => { expect( [400, 422, 408, 429, 500].map((status) => Tidx.isQueryRejection(new Response(null, { status })), ), ).toEqual([true, true, false, false, false]) }) }) describe('isQueryTimeout', () => { test('matches explicit indexer query timeout failures only', () => { expect([ Tidx.isQueryTimeout(appError('Request timeout', 408)), Tidx.isQueryTimeout(appError('query timed out')), Tidx.isQueryTimeout(appError('error sending request for url (...)')), Tidx.isQueryTimeout(appError('db error')), Tidx.isQueryTimeout(appError('Request timeout', 429)), Tidx.isQueryTimeout(new DOMException('query timed out', 'TimeoutError')), ]).toEqual([true, true, true, false, false, false]) }) }) describe('escape', () => { test('doubles quotes for ANSI string literals, leaving backslashes alone', () => { expect(Tidx.escape('USD')).toMatchInlineSnapshot(`"USD"`) expect(Tidx.escape("US' OR '1'='1")).toMatchInlineSnapshot(`"US'' OR ''1''=''1"`) expect(Tidx.escape('back\\slash')).toMatchInlineSnapshot(`"back\\slash"`) expect(Tidx.escape("\\'")).toMatchInlineSnapshot(`"\\''"`) }) }) describe('withTransientRetry', () => { test('retries a transient `db error` and resolves once it succeeds', async () => { vi.useFakeTimers() try { const result = { hasMore: false, rows: [] } as never const fetch = vi .fn() .mockRejectedValueOnce(appError('db error')) .mockRejectedValueOnce(appError('db error')) .mockResolvedValueOnce(result) const client = Tidx.withTransientRetry(fakeClient(fetch)) const promise = client.fetch({ chainId: 42431, query: 'SELECT 1' }) await vi.runAllTimersAsync() await expect(promise).resolves.toBe(result) expect(fetch).toHaveBeenCalledTimes(3) } finally { vi.useRealTimers() } }) test('retries a dropped ClickHouse Cloud connection', async () => { vi.useFakeTimers() try { const result = { hasMore: false, rows: [] } as never const fetch = vi .fn() .mockRejectedValueOnce( appError('ClickHouse HTTP request failed: error sending request for url (…)'), ) .mockResolvedValueOnce(result) const client = Tidx.withTransientRetry(fakeClient(fetch)) const promise = client.fetch({ chainId: 42431, query: 'SELECT 1' }) await vi.runAllTimersAsync() await expect(promise).resolves.toBe(result) expect(fetch).toHaveBeenCalledTimes(2) } finally { vi.useRealTimers() } }) test('retries HTTP-level transient failures under the shared budget', async () => { vi.useFakeTimers() try { const result = { hasMore: false, rows: [] } as never const fetch = vi .fn() .mockRejectedValueOnce(appError('bad gateway', 502)) .mockRejectedValueOnce(appError('too many requests', 429)) .mockResolvedValueOnce(result) const client = Tidx.withTransientRetry(fakeClient(fetch)) const promise = client.fetch({ chainId: 42431, query: 'SELECT 1' }) await vi.runAllTimersAsync() await expect(promise).resolves.toBe(result) expect(fetch).toHaveBeenCalledTimes(3) } finally { vi.useRealTimers() } }) test('disables the inner client retries so the budget is not multiplied', async () => { const fetch = vi.fn().mockResolvedValue({ hasMore: false, rows: [] } as never) const client = Tidx.withTransientRetry(fakeClient(fetch)) await client.fetch({ chainId: 42431, query: 'SELECT 1' }) const { signal: _signal, ...options } = fetch.mock.calls[0]![0] expect(options).toMatchInlineSnapshot(` { "chainId": 42431, "query": "SELECT 1", "retryCount": 1, } `) }) test('does not retry a deterministic application error', async () => { const fetch = vi.fn().mockRejectedValue(appError('syntax error near "SELCT"')) const client = Tidx.withTransientRetry(fakeClient(fetch)) await expect(client.fetch({ chainId: 42431, query: 'SELCT 1' })).rejects.toThrow('syntax error') expect(fetch).toHaveBeenCalledTimes(1) }) test('does not retry an HTTP 422 `db error` (deterministic query-shape failure)', async () => { const fetch = vi.fn().mockRejectedValue(appError('db error', 422)) const client = Tidx.withTransientRetry(fakeClient(fetch)) await expect(client.fetch({ chainId: 42431, query: 'SELECT 1' })).rejects.toThrow('db error') expect(fetch).toHaveBeenCalledTimes(1) }) test('gives up after exhausting retries on persistent `db error`', async () => { vi.useFakeTimers() try { const fetch = vi.fn().mockRejectedValue(appError('db error')) const client = Tidx.withTransientRetry(fakeClient(fetch)) const promise = client.fetch({ chainId: 42431, query: 'SELECT 1' }) const assertion = expect(promise).rejects.toThrow('db error') await vi.runAllTimersAsync() await assertion // First attempt plus four retries. expect(fetch).toHaveBeenCalledTimes(5) } finally { vi.useRealTimers() } }) }) describe('isDeterministicError', () => { test('flags indexer failures a retry cannot fix', () => { // The planner rejects the query shape outright (e.g. 422 `db error`). expect(Tidx.isDeterministicError(appError('db error', 422))).toBe(true) expect(Tidx.isDeterministicError(appError('syntax error near "SELCT"'))).toBe(true) // Transient failures retry instead. expect(Tidx.isDeterministicError(appError('db error'))).toBe(false) expect(Tidx.isDeterministicError(appError('bad gateway', 502))).toBe(false) // Non-indexer errors are not indexer failures at all. expect(Tidx.isDeterministicError(new Error('db error'))).toBe(false) }) }) describe('resolve', () => { test('defaults to the per-chain indexer URL', () => { expect(Tidx.resolve(undefined, { chainId: 4217, principal: null })).toMatchInlineSnapshot(` { "baseUrl": "https://indexer.tempo.xyz", "basicAuth": undefined, } `) expect(Tidx.resolve(undefined, { chainId: 42431, principal: null })).toMatchInlineSnapshot(` { "baseUrl": "https://indexer.testnet.tempo.xyz", "basicAuth": undefined, } `) }) test('uses a static base URL override', () => { expect(Tidx.resolve({ baseUrl: 'https://indexer.test' }, { chainId: 4217, principal: null })) .toMatchInlineSnapshot(` { "baseUrl": "https://indexer.test", "basicAuth": undefined, } `) }) test('uses a base URL resolver override', () => { expect( Tidx.resolve( { baseUrl: (chainId) => `https://${chainId}.indexer.test` }, { chainId: 42431, principal: null }, ), ).toMatchInlineSnapshot(` { "baseUrl": "https://42431.indexer.test", "basicAuth": undefined, } `) }) test('selects a serialized base URL by chain id', () => { const tidx = { baseUrl: JSON.stringify({ 4217: 'https://mainnet.indexer.test', 42431: 'https://testnet.indexer.test' }), // prettier-ignore } expect(Tidx.resolve(tidx, { chainId: 4217, principal: null })).toMatchInlineSnapshot(` { "baseUrl": "https://mainnet.indexer.test", "basicAuth": undefined, } `) expect(Tidx.resolve(tidx, { chainId: 42431, principal: null })).toMatchInlineSnapshot(` { "baseUrl": "https://testnet.indexer.test", "basicAuth": undefined, } `) }) test('passes Zone metadata to the resolver', () => { expect( Tidx.resolve(({ zone }) => ({ baseUrl: zone ? 'http://tidx:8080' : undefined }), { chainId: Zones.zoneModeratoInternal.id, principal: null, zone: Zones.zoneModeratoInternal, }), ).toMatchInlineSnapshot(` { "baseUrl": "http://tidx:8080", "basicAuth": undefined, } `) }) test('the static form ignores the principal', () => { const tidx = { auth: 'user:pass', baseUrl: 'https://internal.indexer' } expect(Tidx.resolve(tidx, { chainId: 4217, principal: publicPrincipal })) .toMatchInlineSnapshot(` { "baseUrl": "https://internal.indexer", "basicAuth": "user:pass", } `) expect(Tidx.resolve(tidx, { chainId: 4217, principal: apiKeyPrincipal })) .toMatchInlineSnapshot(` { "baseUrl": "https://internal.indexer", "basicAuth": "user:pass", } `) }) test('resolves Bearer auth', () => { expect( Tidx.resolve( { auth: 'zone-secret', baseUrl: 'https://internal.indexer' }, { chainId: 4217, principal: apiKeyPrincipal }, ), ).toMatchInlineSnapshot(` { "baseUrl": "https://internal.indexer", "basicAuth": undefined, "bearerAuth": "zone-secret", } `) }) test('selects Bearer auth by chain id', () => { const tidx = { auth: { 4217: 'mainnet-secret', 42431: 'testnet-secret' }, baseUrl: 'https://internal.indexer', } expect(Tidx.resolve(tidx, { chainId: 4217, principal: null }).bearerAuth).toBe('mainnet-secret') expect(Tidx.resolve(tidx, { chainId: 42431, principal: null }).bearerAuth).toBe( 'testnet-secret', ) expect(Tidx.resolve(tidx, { chainId: 421_700_001, principal: null }).bearerAuth).toBeUndefined() }) test('parses serialized Bearer auth by chain id', () => { const tidx = { auth: JSON.stringify({ 421_700_001: 'zone-secret' }), baseUrl: 'https://internal.indexer', } expect(Tidx.resolve(tidx, { chainId: 421_700_001, principal: null }).bearerAuth).toBe( 'zone-secret', ) expect(Tidx.resolve(tidx, { chainId: 4217, principal: null }).bearerAuth).toBeUndefined() }) test('the resolver form routes anonymous callers to a separate upstream', () => { const tidx = ({ principal }: Viem.resolveRpc.Context) => principal?.type === 'public' ? { baseUrl: 'https://indexer.tempo.xyz' } : { auth: 'user:pass', baseUrl: 'https://internal.indexer' } expect(Tidx.resolve(tidx, { chainId: 4217, principal: publicPrincipal })) .toMatchInlineSnapshot(` { "baseUrl": "https://indexer.tempo.xyz", "basicAuth": undefined, } `) expect(Tidx.resolve(tidx, { chainId: 4217, principal: apiKeyPrincipal })) .toMatchInlineSnapshot(` { "baseUrl": "https://internal.indexer", "basicAuth": "user:pass", } `) // A null principal (trusted poller) takes the non-public branch. expect(Tidx.resolve(tidx, { chainId: 4217, principal: null })).toMatchInlineSnapshot(` { "baseUrl": "https://internal.indexer", "basicAuth": "user:pass", } `) }) test('a resolver that omits baseUrl falls back to the built-in public host', () => { const tidx = () => ({}) expect(Tidx.resolve(tidx, { chainId: 42431, principal: publicPrincipal })) .toMatchInlineSnapshot(` { "baseUrl": "https://indexer.testnet.tempo.xyz", "basicAuth": undefined, } `) }) }) describe('getClient', () => { test('sends Bearer auth', async () => { const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response(JSON.stringify({ columns: [], ok: true, row_count: 0, rows: [] }), { headers: { 'content-type': 'application/json' }, }), ) try { const client = Tidx.getClient({ chainId: 4217, tidx: { auth: 'zone-secret', baseUrl: 'https://internal.indexer' }, }) await client.fetch({ query: 'select 1' }) const upstream = fetch.mock.calls[0]![0] as Request expect(upstream.headers.get('authorization')).toBe('Bearer zone-secret') } finally { fetch.mockRestore() } }) test('sends Zone headers', async () => { const fetch = vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response(JSON.stringify({ columns: [], ok: true, row_count: 0, rows: [] }), { headers: { 'content-type': 'application/json' }, }), ) try { const client = Tidx.getClient({ chainId: Zones.zoneModeratoInternal.id, tidx: { baseUrl: 'https://internal.indexer', zoneHeaders: { 'CF-Access-Client-Id': 'client-id', 'CF-Access-Client-Secret': 'client-secret', }, }, zone: Zones.zoneModeratoInternal, }) await client.fetch({ query: 'select 1' }) const upstream = fetch.mock.calls[0]![0] as Request expect(upstream.headers.get('CF-Access-Client-Id')).toBe('client-id') expect(upstream.headers.get('CF-Access-Client-Secret')).toBe('client-secret') } finally { fetch.mockRestore() } }) }) describe('createGetClient', () => { test('memoizes one client per resolved upstream', () => { const getTidx = Tidx.createGetClient({ tidx: ({ principal }) => principal?.type === 'public' ? { baseUrl: 'https://indexer.tempo.xyz' } : { auth: 'user:pass', baseUrl: 'https://internal.indexer' }, }) // Callers resolving to the same upstream share a client; distinct upstreams // (and chain ids) get distinct clients. expect(getTidx(4217, { principal: apiKeyPrincipal })).toBe( getTidx(4217, { principal: apiKeyPrincipal }), ) expect(getTidx(4217, { principal: publicPrincipal })).toBe( getTidx(4217, { principal: publicPrincipal }), ) expect(getTidx(4217, { principal: publicPrincipal })).not.toBe( getTidx(4217, { principal: apiKeyPrincipal }), ) expect(getTidx(4217, { principal: apiKeyPrincipal })).not.toBe( getTidx(42431, { principal: apiKeyPrincipal }), ) }) test('shares a client across principals when the resolver ignores them', () => { const getTidx = Tidx.createGetClient() // With no resolver the upstream is identical for every caller, so the key // (resolved output) collapses and all principals share one client. expect(getTidx(4217, { principal: publicPrincipal })).toBe( getTidx(4217, { principal: apiKeyPrincipal }), ) expect(getTidx(4217, { principal: null })).toBe(getTidx(4217, { principal: publicPrincipal })) }) test('does not share clients across Bearer credentials', () => { const getTidx = Tidx.createGetClient({ tidx: ({ principal }) => ({ auth: principal?.type === 'public' ? 'public-secret' : 'private-secret', baseUrl: 'https://internal.indexer', }), }) expect(getTidx(4217, { principal: publicPrincipal })).not.toBe( getTidx(4217, { principal: apiKeyPrincipal }), ) }) test('consults the resolver on every call but builds one client per upstream', () => { const seen: (string | undefined)[] = [] const getTidx = Tidx.createGetClient({ tidx: ({ principal }) => { const config = principal?.type === 'public' ? { baseUrl: 'https://indexer.tempo.xyz' } : { auth: 'user:pass', baseUrl: 'https://internal.indexer' } seen.push(principal?.type) return config }, }) const first = getTidx(4217, { principal: apiKeyPrincipal }) const second = getTidx(4217, { principal: apiKeyPrincipal }) getTidx(4217, { principal: publicPrincipal }) // The resolver is consulted on every call (it derives the cache key from its // resolved output) — including the repeat that hits the cache — so a // per-request principal is always honored. expect(seen).toContain('api_key') expect(seen).toContain('public') expect(seen.length).toBeGreaterThanOrEqual(3) // …but the client itself (the costly construction) is built once per upstream. expect(first).toBe(second) }) })