[READ-ONLY] Mirror of https://github.com/probablykasper/starchart. GitHub star history graph starchart.kasper.space
chart github github-api github-star graph history star star-history stargazers stars
0

Configure Feed

Select the types of activity you want to include in your feed.

Fix concurrency

Kasper (May 14, 2025, 12:03 AM +0200) 8904789d 4d436cd6

+56 -41
+1
package-lock.json
··· 12 12 "@sveltejs/adapter-cloudflare": "^5.0.3", 13 13 "@sveltejs/kit": "^2.19.0", 14 14 "@sveltejs/vite-plugin-svelte": "^5.0.3", 15 + "bottleneck": "^2.19.5", 15 16 "eslint": "^9.22.0", 16 17 "eslint-config-prettier": "^10.1.1", 17 18 "eslint-plugin-svelte": "^3.0.3",
+1
package.json
··· 16 16 "@sveltejs/adapter-cloudflare": "^5.0.3", 17 17 "@sveltejs/kit": "^2.19.0", 18 18 "@sveltejs/vite-plugin-svelte": "^5.0.3", 19 + "bottleneck": "^2.19.5", 19 20 "eslint": "^9.22.0", 20 21 "eslint-config-prettier": "^10.1.1", 21 22 "eslint-plugin-svelte": "^3.0.3",
+5 -14
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { fly, slide } from 'svelte/transition' 3 - import { fetch_stargazers_page } from './github' 3 + import { errors, fetch_stargazers_page } from './github' 4 4 import { onMount, tick } from 'svelte' 5 5 import '../app.sass' 6 6 import Nav from './Nav.svelte' ··· 16 16 17 17 async function get_stargazers(owner: string, repo: string) { 18 18 if (!chart || !$chart) { 19 - add_error('Chart not initialized') 19 + errors.push('Chart not initialized') 20 20 return 21 21 } 22 22 for (const line of $chart.lines) { ··· 38 38 } 39 39 const { error, stargazers } = await fetch_stargazers_page(owner, repo, 'forward', end_cursor) 40 40 if (!stargazers) { 41 - add_error(error) 41 + errors.push(error) 42 42 chart.deleteLine(line) 43 43 return 44 44 } ··· 66 66 chart.save() 67 67 } 68 68 69 - let errors: { id: number; msg: string }[] = [] 70 - function add_error(msg: string) { 71 - const id = Math.random() 72 - errors.push({ id, msg }) 73 - errors = errors 74 - return id 75 - } 76 - 77 69 let width: number 78 70 let height: number 79 71 $: if (width) { ··· 113 105 <Nav bind:owner bind:repo on_submit={() => get_stargazers(owner, repo)} /> 114 106 115 107 <main bind:this={main_el}> 116 - {#each errors as error, i (error.id)} 108 + {#each $errors as error, i (error.id)} 117 109 <div class="error-container" transition:slide={{ duration: 200 }}> 118 110 <div class="error" transition:fly={{ duration: 200, opacity: 0, y: -40 }}> 119 111 {error.msg} ··· 121 113 type="button" 122 114 aria-label="Dismiss error" 123 115 on:click={() => { 124 - errors.splice(i, 1) 125 - errors = errors 116 + errors.remove_index(i) 126 117 }} 127 118 > 128 119 <!-- Lucide download icon -->
+49 -27
src/routes/github.ts
··· 3 3 import { PUBLIC_PAT } from '$env/static/public' 4 4 import { get, writable } from 'svelte/store' 5 5 import { browser } from '$app/environment' 6 - import { throttling } from '@octokit/plugin-throttling' 6 + import { throttling, type ThrottlingOptions } from '@octokit/plugin-throttling' 7 7 import { retry } from '@octokit/plugin-retry' 8 + // @ts-expect-error import missing from bottleneck's package.json 9 + import BottleneckLight from 'bottleneck/light.js' 10 + const Bottleneck = BottleneckLight as typeof import('bottleneck').default 8 11 9 12 const loaded_token = browser ? localStorage.getItem('starchart-token') : undefined 10 13 export const token = writable(loaded_token || '') ··· 18 21 }) 19 22 } 20 23 21 - export const errors = writable<string[]>([]) 24 + let next_error_id = 0 25 + const errors_store = writable<{ id: number; msg: string }[]>([]) 26 + export const errors = { 27 + subscribe: errors_store.subscribe, 28 + push(msg: string) { 29 + errors_store.update((errors) => { 30 + errors.push({ id: next_error_id++, msg }) 31 + return errors 32 + }) 33 + }, 34 + remove_index(index: number) { 35 + errors_store.update((errors) => { 36 + errors.splice(index, 1) 37 + return errors 38 + }) 39 + }, 40 + } 41 + 42 + const rate_limit_handler: ThrottlingOptions['onRateLimit'] = ( 43 + retry_after, 44 + options, 45 + octokit, 46 + retry_count, 47 + ) => { 48 + const retry_message = ` Retrying in ${retry_after} seconds.` 49 + const max_retries = 3 50 + if (retry_count < max_retries) { 51 + errors.push(`Rate limit reached.${retry_message}`) 52 + return true // retry 53 + } 54 + errors.push(`Rate limit reached ${max_retries} times.`) 55 + } 22 56 23 57 const MyOctokit = Octokit.plugin(throttling, retry) 24 58 const octokit = new MyOctokit({ 25 59 auth: get(token) || PUBLIC_PAT, 26 60 throttle: { 27 - onRateLimit(retry_after, options, octokit, retry_count) { 28 - console.log('onRateLimi', retry_after, options, octokit, retry_count) 29 - const retry_message = ` Retrying in ${retry_after} seconds.` 30 - const max_retries = 3 31 - if (retry_count < max_retries) { 32 - errors.update((errors) => { 33 - errors.push(`Rate limit reached.${retry_message}`) 34 - return errors 35 - }) 36 - return true 37 - } 38 - errors.update((errors) => { 39 - errors.push(`Rate limit reached ${max_retries} times.`) 40 - return errors 41 - }) 42 - }, 43 - onSecondaryRateLimit(retry_after, options, octokit, retry_count) { 44 - console.log('onSecondaryRateLimi', retry_after, options, octokit, retry_count) 45 - const retry_message = `Retrying in ${retry_after} seconds.` 46 - errors.update((errors) => { 47 - errors.push(`Rate limit reached.${retry_message}`) 48 - return errors 49 - }) 50 - }, 61 + onRateLimit: rate_limit_handler, 62 + onSecondaryRateLimit: rate_limit_handler, 63 + // The `write` group is what @octokit/plugin-throttling uses for the GraphQL API 64 + // This property is undocumented, but it is typed at least 65 + write: new Bottleneck.Group({ 66 + maxConcurrent: 10, 67 + minTime: 100, 68 + // from @octokit/plugin-throttling source code: 69 + id: 'octokit-write', 70 + timeout: 1000 * 60 * 2, 71 + }), 51 72 }, 52 73 }) 53 74 ··· 57 78 direction: 'forward' | 'back', 58 79 cursor?: string, 59 80 ) { 81 + const start_time = Date.now() 60 82 const response_promise = octokit.graphql<{ 61 83 repository: { 62 84 stargazers: { ··· 109 131 } 110 132 }) 111 133 112 - console.log('response', response) 134 + console.log('response took', Date.now() - start_time, response) 113 135 if ('error' in response) { 114 136 return { error: response.error, stargazers: undefined } 115 137 } else {