@@ -4,7 +4,7 @@ import { mkdir, writeFile } from "node:fs/promises"
44import { dirname } from "node:path"
55import { PNG } from "pngjs"
66import { err , ok , type Result } from "./result.js"
7- import { Pixel , RandomPixel } from "./types.js"
7+ import { Pixel , RandomPixel , type HeaderMap } from "./types.js"
88import { type } from "arktype"
99
1010/**
@@ -26,15 +26,27 @@ export default class WplaceAPI {
2626
2727 /**
2828 * Download a tile and save it to a file
29+ *
30+ * Ok(true) means the tile was downloaded and saved successfully.
31+ *
32+ * Ok(false) means the tile was empty (404).
33+ *
34+ * Err(Error) means there was an error during the process.
2935 * @param tileX x-coordinate of tile
3036 * @param tileY y-coordinate of tile
3137 * @param path path to save the tile to
32- * @returns Ok(void ) on success, Err(Error) on failure
38+ * @returns Ok(boolean ) on success, Err(Error) on failure
3339 */
34- async downloadTile ( tileX : number , tileY : number , path : string ) : Promise < Result < void , Error > > {
35- const res = await this . getPlain ( format ( ROUTES . GET_TILE , tileX , tileY ) )
40+ async downloadTile ( tileX : number , tileY : number , path : string ) : Promise < Result < boolean , Error > > {
41+ const result = await this . getPlain ( format ( ROUTES . GET_TILE , tileX , tileY ) )
42+ if ( ! result . ok ) return result
43+
44+ const res = result . value
3645
3746 if ( ! res . ok ) {
47+ if ( res . status == 404 ) {
48+ return ok ( false ) // tile empty
49+ }
3850 return err ( new Error ( `Failed to download tile. Status: ${ res . status } ` ) )
3951 }
4052
@@ -43,31 +55,40 @@ export default class WplaceAPI {
4355
4456 await saveFile ( path , buffer )
4557
46- console . log ( `Downloaded tile at (${ tileX } :${ tileY } ) to ${ path } ` )
47-
48- return ok ( )
58+ return ok ( true )
4959 }
5060
5161 /**
5262 * Get a tile as a PNG object
63+ *
64+ * Ok(PNG) means the tile was downloaded successfully.
65+ *
66+ * Ok(undefined) means the tile was empty (404).
67+ *
68+ * Err(Error) means there was an error during the process.
69+ *
5370 * @param tileX x-coordinate of tile
5471 * @param tileY y-coordinate of tile
55- * @returns Ok(PNG) on success, Err(Error) on failure
72+ * @returns Ok(PNG | undefined ) on success, Err(Error) on failure
5673 * @see {@link https://www.npmjs.com/package/pngjs }
5774 */
58- async getTile ( tileX : number , tileY : number ) : Promise < Result < PNG , Error > > {
59- const res = await this . getPlain ( format ( ROUTES . GET_TILE , tileX , tileY ) )
75+ async getTile ( tileX : number , tileY : number ) : Promise < Result < PNG | undefined , Error > > {
76+ const result = await this . getPlain ( format ( ROUTES . GET_TILE , tileX , tileY ) )
77+ if ( ! result . ok ) return result
78+
79+ const res = result . value
6080
6181 if ( ! res . ok ) {
82+ if ( res . status == 404 ) {
83+ return ok ( undefined ) // tile empty
84+ }
6285 return err ( new Error ( `Failed to download tile. Status: ${ res . status } ` ) )
6386 }
6487
6588 const arrayBuffer = await res . arrayBuffer ( )
6689 const buffer = Buffer . from ( arrayBuffer )
6790 const png = PNG . sync . read ( buffer )
6891
69- console . log ( `Got tile at (${ tileX } :${ tileY } )` )
70-
7192 return ok ( png )
7293 }
7394
@@ -128,15 +149,30 @@ export default class WplaceAPI {
128149 * @param route API route to fetch
129150 * @returns Response object
130151 */
131- private async getPlain ( route : string ) : Promise < Response > {
132- console . group ( `GET ${ route } ` )
133- const res = await fetch ( `${ this . options . API_ROOT } ${ route } ` , {
134- method : "GET"
135- } )
136- console . log ( `${ res . status } ${ res . statusText } (${ res . headers . get ( "Content-Type" ) } )` )
137-
138- console . groupEnd ( )
139- return res
152+ private async getPlain ( route : string , headers : HeaderMap = { } ) : Promise < Result < Response , Error > > {
153+ let res : Response
154+
155+ try {
156+ res = await fetch ( `${ this . options . API_ROOT } ${ route } ` , {
157+ method : "GET" ,
158+ headers : {
159+ ...headers ,
160+ "User-Agent" : this . options . userAgent
161+ }
162+ } )
163+ } catch ( e ) {
164+ return err ( e as Error )
165+ }
166+
167+ if ( ! res . ok && res . status == 429 ) {
168+ const retryAfter = res . headers . get ( "Retry-After" )
169+ const waitTime = retryAfter ? parseInt ( retryAfter ) * 1000 : this . options . defaultRetryAfter
170+ //console.log(`Rate limited. Retrying after ${waitTime}ms...`)
171+
172+ return sleep ( waitTime ) . then ( ( ) => this . getPlain ( route , headers ) )
173+ }
174+
175+ return ok ( res )
140176 }
141177
142178 /**
@@ -145,23 +181,18 @@ export default class WplaceAPI {
145181 * @param headers optional HTTP headers
146182 * @returns Ok({ res, data }) on success, Err(Error) on failure
147183 */
148- private async get ( route : string , headers ?: Headers ) : Promise < Result < { res : Response , data : unknown } , Error > > {
149- console . group ( `GET ${ route } ` )
150- const res = await fetch ( `${ this . options . API_ROOT } ${ route } ` , {
151- method : "GET" ,
152- headers : headers ?? { }
153- } )
154- console . log ( `${ res . status } ${ res . statusText } (${ res . headers . get ( "Content-Type" ) } )` )
184+ private async get ( route : string , headers ?: HeaderMap ) : Promise < Result < { res : Response , data : unknown } , Error > > {
185+ const result = await this . getPlain ( route , headers )
186+ if ( ! result . ok ) return result
187+
188+ const res = result . value
155189
156190 try {
157191 const data = await res . json ( )
158192 return ok ( { res, data } )
159193 } catch ( e ) {
160- console . log ( "JSON parse failed." )
161194 return err ( new Error ( "Failed to parse response as JSON." , { cause : e } ) )
162- } finally {
163- console . groupEnd ( )
164- }
195+ }
165196 }
166197
167198}
@@ -172,11 +203,17 @@ export default class WplaceAPI {
172203export type APIOptions = {
173204 /** Base URL for the API */
174205 API_ROOT : string
206+ /** User-Agent used for HTTP requests */
207+ userAgent : string
208+ /** Default time in MS to wait before retrying request after 429 response */
209+ defaultRetryAfter : number
175210}
176211
177212/** Default options for the Wplace API client */
178213const DEFAULT_API_OPTIONS : APIOptions = {
179- API_ROOT : "https://backend.wplace.live"
214+ API_ROOT : "https://backend.wplace.live" ,
215+ userAgent : "wplace-api-client/0.1" ,
216+ defaultRetryAfter : 20000
180217}
181218
182219function compileOptions < OptionsType extends object > ( options : Partial < OptionsType > | undefined , defaultOptions : OptionsType ) : OptionsType {
@@ -202,4 +239,8 @@ async function saveFile(path: string, data: Buffer) {
202239 } catch ( e ) {
203240 throw new Error ( `Failed to write tile to ${ path } .` , { cause : e } )
204241 }
242+ }
243+
244+ function sleep ( ms : number ) {
245+ return new Promise ( resolve => setTimeout ( resolve , ms ) )
205246}
0 commit comments