···66 let showRepoStats = $state(false);
77 let did = $state('');
88 let pdsUrl = $state('');
99+ let slowPoke = $state(false);
9101010- const resolvedResult = (didResult: string, pdsUrlResult: string) => {
1111+ const resolvedResult = (didResult: string, pdsUrlResult: string, slowPokeResult: boolean) => {
1112 did = didResult;
1213 pdsUrl = pdsUrlResult;
1414+ slowPoke = slowPokeResult;
1315 showRepoStats = true;
1416 };
1517···1719</script>
18201921<main>
2020- <h1>Repo Walk Example</h1>
2222+ {#if showRepoStats}
2323+ {#if slowPoke}
2424+ <h2>Walking the repo via api calls</h2>
2525+ {:else}
2626+ <h2>Walking the repo via repo export</h2>
2727+ {/if}
2828+ {:else}
2929+ <h1>Repo Walk Example</h1>
3030+ <br>
3131+ <p>Demo showing why you may rather export the users whole repo instead of walking it via api calls</p>
21323333+ <sub>Also shows how many records you have and how many of each kind if you're into that kind of thing...</sub>
3434+ {/if}
2235 <div class="card">
2336 {#if showRepoStats}
2424- <RepoStats did={did} pdsUrl={pdsUrl}/>
3737+ <RepoStats did={did} pdsUrl={pdsUrl} slowPokeMode={slowPoke}/>
2538 {:else}
2639 <SearchForm resolvedResult={resolvedResult}/>
2740 {/if}
+119-22
src/lib/RepoStats.svelte
···55 import { Client, simpleFetchHandler } from '@atcute/client';
66 import type {} from '@atcute/atproto';
7788- const { did, pdsUrl } = $props();
88+ const { did, pdsUrl, slowPokeMode } = $props();
991010 interface CountedCollection {
1111 collection: string;
1212 count: number;
1313 }
14141515+ //Shared State
1516 let loading = $state(true);
1717+ let error: string | null = $state(null);
1818+ //Downloaded stuff
1619 let downloadedBytes = $state(0);
1720 let downloadedMB = $derived((downloadedBytes / (1024 * 1024)).toFixed(2));
1818- let error: string | null = $state(null);
2121+ //Ui counts for collections
1922 let collections = $state(new Array<CountedCollection>());
2023 let collectionsOrdered: Array<CountedCollection> = $derived([...collections].sort((a, b) => b.count - a.count));
2124 let totalRecords = $state(0);
2525+ let currentCollection: string | null = $state(null);
2626+ //Timer stuff
2227 let startTime = $state<number | null>(null);
2328 let endTime = $state<number | null>(null);
2429 let elapsedTime = $state('');
2525-2630 let interval = $state<number | null>(null);
3131+ //Just for the slow pokes
3232+ let webCalls = $state(0);
27332834 const calculateElapsedTime = () => {
2935 if (!startTime) return '0.00';
···3238 };
33393440 const startTimer = () => {
4141+ endTime = null;
4242+ startTime = Date.now();
3543 interval = setInterval(() => {
3644 calculateElapsedTime();
3745 }, 250);
3846 };
39474848+ const stopTimer = () => {
4949+ if (interval) {
5050+ clearInterval(interval);
5151+ }
5252+ endTime = Date.now();
5353+ calculateElapsedTime();
5454+ };
5555+4056 // Calls the getRepo endpoint to get a .car export to walk the repo
4157 const getRepoStatsViaExport = async () => {
4258 const rpc = new Client({ handler: simpleFetchHandler({ service: pdsUrl }) });
4343-4444- startTime = Date.now();
4545- endTime = null;
4659 startTimer();
4760 try {
4861 const result = await rpc.get('com.atproto.sync.getRepo', {
···5568 }
56695770 let stream = result.data;
5858-5959-6071 const car = fromStream(stream);
61726273 try {
6374 for await (const entry of car) {
6464- const data = CBOR.decode(entry.bytes);
6565- if (!data.$type) {
7575+ const record = CBOR.decode(entry.bytes);
7676+ if (!record.$type) {
7777+ //Is not a record
6678 continue;
6779 }
68806969- let checkForCollection = collections.find(c => c.collection === data.$type);
8181+ let checkForCollection = collections.find(c => c.collection === record.$type);
7082 if (!checkForCollection) {
7171- collections.push({ collection: data.$type, count: 1 });
8383+ collections.push({ collection: record.$type, count: 1 });
7284 } else {
7385 checkForCollection.count++;
7486 }
···7688 totalRecords++;
7789 }
7890 } finally {
7979- if (interval) {
8080- clearInterval(interval);
8181- interval = null;
8282- calculateElapsedTime();
8383- }
9191+ stopTimer();
8492 await car.dispose();
8593 }
86948787- endTime = Date.now();
8895 loading = false;
8996 } catch (err) {
9090- endTime = Date.now();
9797+ stopTimer();
9198 console.error('Error fetching repo stats:', err);
9299 if (err instanceof Error) {
93100 error = err.message;
···98105 }
99106 };
100107108108+ //An ungodly amount of api calls showing the speed difference between getting an export and walking the repo
109109+ //via api calls
110110+ const getRepoStatsTheLongWay = async () => {
111111+ try {
112112+ const rpc = new Client({ handler: simpleFetchHandler({ service: pdsUrl }) });
113113+ startTimer();
114114+ let describeRepo = await rpc.get('com.atproto.repo.describeRepo', {
115115+ params: {
116116+ repo: did
117117+ }
118118+ });
119119+ if(!describeRepo.ok){
120120+ throw new Error(`HTTP error! status: ${describeRepo.status}`);
121121+ }
122122+ webCalls++;
123123+ for (const collection of describeRepo.data.collections) {
124124+ let totalRecordsInCollection = 0;
125125+ currentCollection = collection;
126126+ const firstCollectionList = await rpc.get('com.atproto.repo.listRecords', {
127127+ params: {
128128+ collection,
129129+ repo: did
130130+ }
131131+ });
132132+ webCalls++;
133133+ if(!firstCollectionList.ok){
134134+ console.error(`HTTP error! status: ${firstCollectionList.status}`);
135135+ continue;
136136+ }
137137+ totalRecords += firstCollectionList.data.records.length;
138138+ totalRecordsInCollection += firstCollectionList.data.records.length;
139139+140140+ let cursor = firstCollectionList.data.cursor;
141141+ do {
142142+ const nextCollectionList = await rpc.get('com.atproto.repo.listRecords', {
143143+ params: {
144144+ collection,
145145+ repo: did,
146146+ cursor
147147+ }
148148+ });
149149+ webCalls++;
150150+ if(!nextCollectionList.ok){
151151+ console.error(`HTTP error! status: ${nextCollectionList.status}`);
152152+ continue;
153153+ }
154154+ totalRecordsInCollection += nextCollectionList.data.records.length;
155155+ cursor = nextCollectionList.data.cursor;
156156+ totalRecords += nextCollectionList.data.records.length;
157157+158158+ } while (cursor !== undefined);
159159+ collections.push({ collection: collection, count:totalRecordsInCollection });
160160+ }
161161+ loading = false;
162162+ stopTimer();
163163+ }
164164+ catch (err) {
165165+ loading = false;
166166+ stopTimer();
167167+ console.error('Error fetching repo stats:', err);
168168+ if (err instanceof Error) {
169169+ error = err.message;
170170+ } else {
171171+ error = 'Unknown error: can check the console for more details';
172172+ }
173173+ }
174174+ };
175175+101176 onMount(() => {
102102- getRepoStatsViaExport();
177177+ if (slowPokeMode) {
178178+ getRepoStatsTheLongWay();
179179+ } else {
180180+ getRepoStatsViaExport();
181181+ }
182182+103183 });
104184105185</script>
106186107187<div>
188188+ {#if slowPokeMode}
189189+ <img alt="A Shellder biting a Slowpoke's tail, as seen in the Pokémon anime "
190190+ src="https://upload.wikimedia.org/wikipedia/en/a/a2/Slowpoke_and_Shellder.jpg">
191191+ <br>
192192+ {/if}
193193+108194 {#if error}
109195 <p style="color: red">{error}</p>
110196 {/if}
111111- {#if loading}
197197+ {#if loading && !slowPokeMode}
112198 Loading... ({downloadedMB} MB downloaded, {elapsedTime}s)
199199+ {:else if loading && slowPokeMode}
200200+ Loading... ({webCalls.toLocaleString()} web calls made, {elapsedTime}s)
113201 {:else}
114114- <span>Repo size {downloadedMB} MB (fetched in {elapsedTime}s)</span>
202202+ {#if !slowPokeMode}
203203+ <span>Repo size {downloadedMB} MB (fetched in {elapsedTime}s)</span>
204204+ {:else}
205205+ <span>Web calls made: {webCalls.toLocaleString()} (fetched in {elapsedTime}s)</span>
206206+ {/if}
115207 {/if}
208208+ {#if loading && currentCollection !== null}
209209+ <br>
210210+ <span>Currently walking collection: {currentCollection}</span>
211211+ {/if}
212212+116213 {#if collectionsOrdered.length > 0}
117214118215 <br>
+4-2
src/lib/SearchForm.svelte
···28282929 let handleToLookUp = $state('');
3030 let error: string | null = $state(null);
3131+ let slowpoke = $state(false);
31323233 let { resolvedResult } = $props();
3334···4647 const didDoc = await didResolver.resolve(did);
4748 const pdsUrl = getPdsEndpoint(didDoc);
48494949- resolvedResult(did, pdsUrl);
5050+ resolvedResult(did, pdsUrl, slowpoke);
5051 }catch(e){
5152 if (e instanceof Error) {
5253 error = e.message;
···6364 <button>Lookup</button>
6465 <br>
6566 <label>
6767+ <input bind:checked={slowpoke} type="checkbox"/>
6668 slowpoke (uses web calls to walk the repository to show you the speed difference)
6767- <input type="checkbox"/>
6969+6870 </label>
6971 {#if error}
7072 <p style="color: red;">Error: {error}</p>