From 5f3402ee5d99f6ad41b38d34ed24791d31f03dbf Mon Sep 17 00:00:00 2001 From: Charles Hetterich Date: Mon, 20 Apr 2026 09:19:00 -0400 Subject: [PATCH 1/3] point to new playground registry --- cdm.json | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/cdm.json b/cdm.json index 740ed00..bd48fb5 100644 --- a/cdm.json +++ b/cdm.json @@ -7,14 +7,14 @@ }, "dependencies": { "acc2c3b5e912b762": { - "@example/playground-registry": "latest" + "@w3s/playground-registry": "latest" } }, "contracts": { "acc2c3b5e912b762": { - "@example/playground-registry": { - "version": 6, - "address": "0x279585Cb8E8971e34520A3ebbda3E0C4D77C3d97", + "@w3s/playground-registry": { + "version": 0, + "address": "0xb9Aa5e8421AF2c7426a37bA10045158dDe981856", "abi": [ { "type": "constructor", @@ -196,6 +196,55 @@ ], "stateMutability": "view" }, + { + "type": "function", + "name": "getApps", + "inputs": [ + { + "name": "start", + "type": "uint32" + }, + { + "name": "count", + "type": "uint32" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "components": [ + { + "name": "total", + "type": "uint32" + }, + { + "name": "entries", + "type": "tuple[]", + "components": [ + { + "name": "index", + "type": "uint32" + }, + { + "name": "domain", + "type": "string" + }, + { + "name": "metadata_uri", + "type": "string" + }, + { + "name": "owner", + "type": "address" + } + ] + } + ] + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "getMetadataUri", @@ -241,7 +290,7 @@ "stateMutability": "view" } ], - "metadataCid": "bafk2bzaceck7veaix4ttzyd6bmwlssgycrrlgilpat2c272nczzlrgnqy6fze" + "metadataCid": "bafk2bzaceb5zctlcjumwv6co6buraoqgatsss3fffmzh6bnlxgemovujkzod4" } } } From fc235d964b089b84baba0a341b70c68321ce0cb1 Mon Sep 17 00:00:00 2001 From: Charles Hetterich Date: Mon, 20 Apr 2026 10:43:12 -0400 Subject: [PATCH 2/3] updated `mod` querying to use new `getApps` to get batches of apps at once --- src/commands/mod/AppBrowser.tsx | 70 +++++++++++++++++++++------------ src/utils/registry.ts | 2 +- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/commands/mod/AppBrowser.tsx b/src/commands/mod/AppBrowser.tsx index 6ae5eb5..12cbec3 100644 --- a/src/commands/mod/AppBrowser.tsx +++ b/src/commands/mod/AppBrowser.tsx @@ -31,37 +31,56 @@ export function AppBrowser({ registry, onSelect }: Props) { const [cursor, setCursor] = useState(0); const [scroll, setScroll] = useState(0); const [fetching, setFetching] = useState(true); - const nextIdx = useRef(null); + // Next ascending-index window to request from `getApps(start, count)`. We + // page high → low (newest-first), so this walks downward: the next call's + // `start` is `prevStart - BATCH` (clamped to 0). `null` means no more. + const nextStart = useRef(null); const gateway = getGateway("paseo"); const loadBatch = useCallback( - async (startIdx: number) => { + async (start: number) => { setFetching(true); - const indices = []; - for (let i = startIdx; i > startIdx - BATCH && i >= 0; i--) indices.push(i); - const lowestQueried = Math.min(...indices); - nextIdx.current = lowestQueried > 0 ? lowestQueried - 1 : null; - - const results = await Promise.all( - indices.map(async (i) => { - const res = await registry.getDomainAt.query(i); - return res.value.isSome ? (res.value.value as string) : null; - }), - ); - - const entries: AppEntry[] = results - .filter((d): d is string => d !== null) - .map((domain) => ({ domain, name: null, description: null, repository: null })); + // Contract returns entries ascending from `start`. We want + // newest-first in the list, so reverse after. `count` may be + // capped short if `start + count > total` — no harm, the + // contract simply returns fewer entries. + const res = await registry.getApps.query(start, BATCH); + const rawEntries = res.value.entries as Array<{ + index: number; + domain: string; + metadata_uri: string; + owner: string; + }>; + // Keep our `total` in sync with the contract's reported value. + // Always calling setTotal is fine — React bails out on same-value + // updates, so this doesn't cause extra renders. + setTotal(res.value.total as number); + + // Sort defensively — we can't rely on the contract's ordering + // guarantees, and every entry carries its own `index`. + const sorted = [...rawEntries].sort((a, b) => b.index - a.index); + + nextStart.current = start > 0 ? Math.max(0, start - BATCH) : null; + + const entries: AppEntry[] = sorted.map((e) => ({ + domain: e.domain, + name: null, + description: null, + repository: null, + })); setApps((prev) => [...prev, ...entries]); setFetching(false); + // Metadata JSONs still have to be fetched one-at-a-time from + // the gateway — that's IPFS HTTP, not a chain query. Kick them + // off in parallel and update each row as it lands. await Promise.allSettled( - entries.map(async (entry) => { - const metaRes = await registry.getMetadataUri.query(entry.domain); - const cid = metaRes.value.isSome ? (metaRes.value.value as string) : null; + sorted.map(async (raw, i) => { + const entry = entries[i]; + const cid = raw.metadata_uri; if (!cid) return; const meta = await fetchJson>(cid, gateway); setApps((prev) => @@ -87,15 +106,16 @@ export function AppBrowser({ registry, onSelect }: Props) { const res = await registry.getAppCount.query(); const count = res.value as number; setTotal(count); - if (count > 0) await loadBatch(count - 1); + if (count > 0) await loadBatch(Math.max(0, count - BATCH)); + else setFetching(false); })(); - }, []); + }, [registry, loadBatch]); useEffect(() => { - if (cursor >= apps.length - 3 && nextIdx.current !== null && !fetching) { - loadBatch(nextIdx.current); + if (cursor >= apps.length - 3 && nextStart.current !== null && !fetching) { + loadBatch(nextStart.current); } - }, [cursor, apps.length, fetching]); + }, [cursor, apps.length, fetching, loadBatch]); useInput((input, key) => { if (key.upArrow && cursor > 0) { diff --git a/src/utils/registry.ts b/src/utils/registry.ts index d302bd5..50ce7d7 100644 --- a/src/utils/registry.ts +++ b/src/utils/registry.ts @@ -18,5 +18,5 @@ export async function getRegistryContract( defaultSigner: signer.signer, defaultOrigin: signer.address, }); - return manager.getContract("@example/playground-registry"); + return manager.getContract("@w3s/playground-registry"); } From 59c0a1cfbb5c838efbe2dfc5a83d31602f3167b6 Mon Sep 17 00:00:00 2001 From: Charles Hetterich Date: Mon, 20 Apr 2026 12:54:16 -0400 Subject: [PATCH 3/3] correct paging direction (was showing oldest first before) --- src/commands/mod/AppBrowser.tsx | 46 ++++++++++++++------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/src/commands/mod/AppBrowser.tsx b/src/commands/mod/AppBrowser.tsx index 12cbec3..0eaadfb 100644 --- a/src/commands/mod/AppBrowser.tsx +++ b/src/commands/mod/AppBrowser.tsx @@ -31,10 +31,11 @@ export function AppBrowser({ registry, onSelect }: Props) { const [cursor, setCursor] = useState(0); const [scroll, setScroll] = useState(0); const [fetching, setFetching] = useState(true); - // Next ascending-index window to request from `getApps(start, count)`. We - // page high → low (newest-first), so this walks downward: the next call's - // `start` is `prevStart - BATCH` (clamped to 0). `null` means no more. - const nextStart = useRef(null); + // Offset (in reverse-chronological order) of the next page to request. + // Contract's `getApps(start, count)` treats `start` as a REVERSE offset — + // `start=0` returns the newest batch, `start=BATCH` the next page, etc. + // `null` = no more pages. + const nextStart = useRef(0); const gateway = getGateway("paseo"); @@ -42,10 +43,6 @@ export function AppBrowser({ registry, onSelect }: Props) { async (start: number) => { setFetching(true); - // Contract returns entries ascending from `start`. We want - // newest-first in the list, so reverse after. `count` may be - // capped short if `start + count > total` — no harm, the - // contract simply returns fewer entries. const res = await registry.getApps.query(start, BATCH); const rawEntries = res.value.entries as Array<{ index: number; @@ -53,18 +50,14 @@ export function AppBrowser({ registry, onSelect }: Props) { metadata_uri: string; owner: string; }>; - // Keep our `total` in sync with the contract's reported value. - // Always calling setTotal is fine — React bails out on same-value - // updates, so this doesn't cause extra renders. - setTotal(res.value.total as number); + const totalFromResp = res.value.total as number; + // Always set — React bails on same-value updates. + setTotal(totalFromResp); - // Sort defensively — we can't rely on the contract's ordering - // guarantees, and every entry carries its own `index`. - const sorted = [...rawEntries].sort((a, b) => b.index - a.index); + // Contract returns newest-first; preserve that order for display. + nextStart.current = start + BATCH < totalFromResp ? start + BATCH : null; - nextStart.current = start > 0 ? Math.max(0, start - BATCH) : null; - - const entries: AppEntry[] = sorted.map((e) => ({ + const entries: AppEntry[] = rawEntries.map((e) => ({ domain: e.domain, name: null, description: null, @@ -78,7 +71,7 @@ export function AppBrowser({ registry, onSelect }: Props) { // the gateway — that's IPFS HTTP, not a chain query. Kick them // off in parallel and update each row as it lands. await Promise.allSettled( - sorted.map(async (raw, i) => { + rawEntries.map(async (raw, i) => { const entry = entries[i]; const cid = raw.metadata_uri; if (!cid) return; @@ -102,14 +95,13 @@ export function AppBrowser({ registry, onSelect }: Props) { ); useEffect(() => { - (async () => { - const res = await registry.getAppCount.query(); - const count = res.value as number; - setTotal(count); - if (count > 0) await loadBatch(Math.max(0, count - BATCH)); - else setFetching(false); - })(); - }, [registry, loadBatch]); + // `getApps(0, BATCH)` returns the newest batch plus `total`, so we + // don't need a separate `getAppCount` probe. When the registry is + // empty, the response still carries `total: 0` — we drop the spinner + // and leave `nextStart.current` at its initial 0 harmlessly (the + // scroll-trigger effect guards on `apps.length`, so it won't re-fire). + loadBatch(0); + }, [loadBatch]); useEffect(() => { if (cursor >= apps.length - 3 && nextStart.current !== null && !fetching) {