From 3b52dc8823d5b3fa50f198911c573fa13fbbb10f Mon Sep 17 00:00:00 2001 From: Bekiboo Date: Thu, 16 Jul 2026 00:28:50 +0300 Subject: [PATCH 1/4] chore(eid-wallet): make `pnpm test` run without Storybook's browser project --- infrastructure/eid-wallet/package.json | 2 +- infrastructure/eid-wallet/src/test/env-static-public.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/infrastructure/eid-wallet/package.json b/infrastructure/eid-wallet/package.json index 2c830ea8b..a7107cb23 100644 --- a/infrastructure/eid-wallet/package.json +++ b/infrastructure/eid-wallet/package.json @@ -14,7 +14,7 @@ "lint": "npx @biomejs/biome lint --write ./src", "check-lint": "npx @biomejs/biome lint ./src", "tauri": "tauri", - "test": "vitest run", + "test": "vitest run --project eid-wallet", "storybook": "svelte-kit sync && storybook dev -p 6006", "build-storybook": "storybook build", "build:apk": "npm run tauri android build -- --apk --target aarch64 --target armv7", diff --git a/infrastructure/eid-wallet/src/test/env-static-public.ts b/infrastructure/eid-wallet/src/test/env-static-public.ts index cf0f58a65..51516bc49 100644 --- a/infrastructure/eid-wallet/src/test/env-static-public.ts +++ b/infrastructure/eid-wallet/src/test/env-static-public.ts @@ -1,6 +1,6 @@ // Test stub for SvelteKit's `$env/static/public` (aliased in vitest.config.ts). // Real values are injected by SvelteKit at build time; tests only need the // symbols to exist so modules that import them can load. -export const PUBLIC_EID_WALLET_TOKEN = ""; -export const PUBLIC_PROVISIONER_URL = ""; -export const PUBLIC_REGISTRY_URL = ""; +export const PUBLIC_EID_WALLET_TOKEN = "test-token"; +export const PUBLIC_PROVISIONER_URL = "http://provisioner.test"; +export const PUBLIC_REGISTRY_URL = "http://registry.test"; From 8d547d6d9d4b0c9d02fb39b2a2026c84aaad2ebd Mon Sep 17 00:00:00 2001 From: Bekiboo Date: Thu, 16 Jul 2026 00:30:08 +0300 Subject: [PATCH 2/4] perf(eid-wallet): stop the Social Bindings full list downloading photo blobs --- .../src/lib/utils/socialBinding.spec.ts | 96 +++++++++++++++++++ .../eid-wallet/src/lib/utils/socialBinding.ts | 47 ++++++++- .../routes/(app)/social-bindings/+page.svelte | 20 +++- 3 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts diff --git a/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts b/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts new file mode 100644 index 000000000..0faa03fcf --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts @@ -0,0 +1,96 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { clearVaultUriCache, resolveVaultUri } from "./socialBinding"; + +const ENAME = "@ada-0000-0000"; +const OTHER = "@grace-1111-1111"; +const VAULT_URI = "http://vault.test:4000"; +const EXPECTED = "http://vault.test:4000/graphql"; + +/** Registry responds with a vault URI; counts how often it was actually hit. */ +function stubRegistry(uri = VAULT_URI) { + const fetchMock = vi.fn(async () => ({ + ok: true, + json: async () => ({ uri }), + })); + vi.stubGlobal("fetch", fetchMock); + return fetchMock; +} + +beforeEach(() => { + clearVaultUriCache(); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("resolveVaultUri — registry lookup memoisation", () => { + it("hits the registry once for repeated sequential lookups", async () => { + const fetchMock = stubRegistry(); + + expect(await resolveVaultUri(ENAME)).toBe(EXPECTED); + expect(await resolveVaultUri(ENAME)).toBe(EXPECTED); + expect(await resolveVaultUri(ENAME)).toBe(EXPECTED); + + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("coalesces concurrent lookups of the same eName into one request", async () => { + const fetchMock = stubRegistry(); + + // This is the real shape of the bug: the binding reconcile and the name + // lookup resolve the same counterparty at the same time, via Promise.all. + const results = await Promise.all([ + resolveVaultUri(ENAME), + resolveVaultUri(ENAME), + resolveVaultUri(ENAME), + resolveVaultUri(ENAME), + ]); + + expect(results).toEqual([EXPECTED, EXPECTED, EXPECTED, EXPECTED]); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("treats a bare eName and an @-prefixed one as the same cache entry", async () => { + const fetchMock = stubRegistry(); + + await resolveVaultUri("ada-0000-0000"); + await resolveVaultUri("@ada-0000-0000"); + + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("keeps distinct eNames independent", async () => { + const fetchMock = stubRegistry(); + + await resolveVaultUri(ENAME); + await resolveVaultUri(OTHER); + + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + + it("does not cache failures — a later lookup retries the registry", async () => { + const failing = vi.fn(async () => ({ ok: false, status: 503 })); + vi.stubGlobal("fetch", failing); + + await expect(resolveVaultUri(ENAME)).rejects.toThrow( + /could not resolve/i, + ); + await expect(resolveVaultUri(ENAME)).rejects.toThrow( + /could not resolve/i, + ); + + expect(failing).toHaveBeenCalledTimes(2); + }); + + it("clearVaultUriCache forces the next lookup back to the registry", async () => { + const fetchMock = stubRegistry(); + + await resolveVaultUri(ENAME); + clearVaultUriCache(); + await resolveVaultUri(ENAME); + + expect(fetchMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts b/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts index 4c3ee74ca..5c5e78948 100644 --- a/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts +++ b/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts @@ -29,11 +29,17 @@ export interface BindingDocEdge { // Registry resolution // --------------------------------------------------------------------------- -/** - * Resolve an eName to its eVault GraphQL endpoint via the registry. - */ -export async function resolveVaultUri(ename: string): Promise { - const normalized = ename.startsWith("@") ? ename : `@${ename}`; +const VAULT_URI_TTL_MS = 5 * 60_000; +const vaultUriCache = new Map(); +const vaultUriInFlight = new Map>(); + +/** Drop the memoised registry lookups (sign-out, tests). */ +export function clearVaultUriCache(): void { + vaultUriCache.clear(); + vaultUriInFlight.clear(); +} + +async function fetchVaultUri(normalized: string): Promise { const url = new URL( `resolve?w3id=${encodeURIComponent(normalized)}`, PUBLIC_REGISTRY_URL, @@ -52,6 +58,37 @@ export async function resolveVaultUri(ename: string): Promise { : new URL("/graphql", base).toString(); } +/** + * Resolve an eName to its eVault GraphQL endpoint via the registry. + * + * Memoised: an eName→URI mapping only changes when a vault is re-provisioned, + * but several layers resolve the same counterparty independently on one screen + * load (the binding reconcile and the name lookup, for a start), so the same + * round trip was being paid 3-4× per contact. Concurrent callers share one + * in-flight request; failures are not cached. + */ +export async function resolveVaultUri(ename: string): Promise { + const normalized = ename.startsWith("@") ? ename : `@${ename}`; + + const cached = vaultUriCache.get(normalized); + if (cached && Date.now() - cached.at < VAULT_URI_TTL_MS) return cached.uri; + + const pending = vaultUriInFlight.get(normalized); + if (pending) return pending; + + const request = fetchVaultUri(normalized) + .then((uri) => { + vaultUriCache.set(normalized, { uri, at: Date.now() }); + return uri; + }) + .finally(() => { + vaultUriInFlight.delete(normalized); + }); + + vaultUriInFlight.set(normalized, request); + return request; +} + // --------------------------------------------------------------------------- // Generic cross-vault GraphQL request // --------------------------------------------------------------------------- diff --git a/infrastructure/eid-wallet/src/routes/(app)/social-bindings/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/social-bindings/+page.svelte index 5e82bee2c..650b52027 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/social-bindings/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/social-bindings/+page.svelte @@ -1,3 +1,11 @@ + +