1+ import type {
2+ ScrapeParams ,
3+ ScrapeResponse ,
4+ JobCreateRequest ,
5+ JobCreateResponse ,
6+ JobStatusResponse ,
7+ ScreenshotRequest ,
8+ ScreenshotCreateResponse ,
9+ WatchCreateRequest ,
10+ WatchCreateResponse ,
11+ WatchGetResponse ,
12+ WatchListResponse ,
13+ WatchDeleteResponse ,
14+ WatchActionResponse ,
15+ } from './types'
16+
17+ export interface ClientOptions {
18+ apiKey : string
19+ baseUrl ?: string
20+ fetchFn ?: typeof fetch
21+ timeoutMs ?: number
22+ }
23+
24+ export class SupacrawlerError extends Error {
25+ status ?: number
26+ body ?: unknown
27+ constructor ( message : string , status ?: number , body ?: unknown ) {
28+ super ( message )
29+ this . status = status
30+ this . body = body
31+ }
32+ }
33+
34+ export class SupacrawlerClient {
35+ private apiKey : string
36+ private baseUrl : string
37+ private fetchFn : typeof fetch
38+ private timeoutMs : number
39+
40+ constructor ( options : ClientOptions ) {
41+ this . apiKey = options . apiKey
42+ this . baseUrl = ( options . baseUrl ?? 'https://api.supacrawler.com/api/v1' ) . replace ( / \/ $ / , '' )
43+ this . fetchFn = options . fetchFn ?? fetch
44+ this . timeoutMs = options . timeoutMs ?? 30000
45+ }
46+
47+ private headers ( ) : HeadersInit {
48+ return { Authorization : `Bearer ${ this . apiKey } ` , 'Content-Type' : 'application/json' }
49+ }
50+
51+ private async request < T > ( path : string , init ?: RequestInit & { timeoutMs ?: number } ) : Promise < T > {
52+ const controller = new AbortController ( )
53+ const timeout = init ?. timeoutMs ?? this . timeoutMs
54+ const id = setTimeout ( ( ) => controller . abort ( ) , timeout )
55+
56+ try {
57+ const res = await this . fetchFn ( `${ this . baseUrl } ${ path } ` , { ...init , signal : controller . signal } )
58+ return await this . handle < T > ( res )
59+ } catch ( e : any ) {
60+ if ( e ?. name === 'AbortError' ) {
61+ throw new SupacrawlerError ( `Request timed out after ${ timeout } ms` )
62+ }
63+ throw e
64+ } finally {
65+ clearTimeout ( id )
66+ }
67+ }
68+
69+ private async handle < T > ( res : Response ) : Promise < T > {
70+ if ( ! res . ok ) {
71+ let body : unknown
72+ try { body = await res . json ( ) } catch { body = await res . text ( ) }
73+ throw new SupacrawlerError ( `HTTP ${ res . status } ` , res . status , body )
74+ }
75+ return res . json ( ) as Promise < T >
76+ }
77+
78+ // ------------- Scrape -------------
79+ async scrape ( params : ScrapeParams ) : Promise < ScrapeResponse > {
80+ const qs = new URLSearchParams ( )
81+ Object . entries ( params ) . forEach ( ( [ k , v ] ) => {
82+ if ( v !== undefined && v !== null ) qs . append ( k , String ( v ) )
83+ } )
84+ return this . request < ScrapeResponse > ( `/scrape?${ qs . toString ( ) } ` , { headers : { Authorization : `Bearer ${ this . apiKey } ` } } )
85+ }
86+
87+ // ------------- Jobs (crawl + status) -------------
88+ async createJob ( req : JobCreateRequest ) : Promise < JobCreateResponse > {
89+ return this . request < JobCreateResponse > ( `/jobs` , {
90+ method : 'POST' ,
91+ headers : this . headers ( ) ,
92+ body : JSON . stringify ( req ) ,
93+ } )
94+ }
95+
96+ async getJob ( jobId : string ) : Promise < JobStatusResponse > {
97+ return this . request < JobStatusResponse > ( `/jobs/${ jobId } ` , { headers : { Authorization : `Bearer ${ this . apiKey } ` } } )
98+ }
99+
100+ async waitForJob ( jobId : string , opts : { intervalMs ?: number ; timeoutMs ?: number } = { } ) : Promise < JobStatusResponse > {
101+ const interval = opts . intervalMs ?? 3000
102+ const timeout = opts . timeoutMs ?? 300000
103+ const start = Date . now ( )
104+ while ( true ) {
105+ const status = await this . getJob ( jobId )
106+ if ( status . status === 'completed' || status . status === 'failed' ) return status
107+ if ( Date . now ( ) - start > timeout ) throw new SupacrawlerError ( `Timeout waiting for job ${ jobId } ` )
108+ await new Promise ( r => setTimeout ( r , interval ) )
109+ }
110+ }
111+
112+ // ------------- Screenshots -------------
113+ async createScreenshotJob ( req : ScreenshotRequest ) : Promise < ScreenshotCreateResponse > {
114+ return this . request < ScreenshotCreateResponse > ( `/screenshots` , {
115+ method : 'POST' ,
116+ headers : this . headers ( ) ,
117+ body : JSON . stringify ( req ) ,
118+ } )
119+ }
120+
121+ // ------------- Watch -------------
122+ async watchCreate ( req : WatchCreateRequest ) : Promise < WatchCreateResponse > {
123+ return this . request < WatchCreateResponse > ( `/watch` , {
124+ method : 'POST' ,
125+ headers : this . headers ( ) ,
126+ body : JSON . stringify ( req ) ,
127+ } )
128+ }
129+
130+ async watchGet ( watchId : string ) : Promise < WatchGetResponse > {
131+ return this . request < WatchGetResponse > ( `/watch/${ watchId } ` , { headers : { Authorization : `Bearer ${ this . apiKey } ` } } )
132+ }
133+
134+ async watchList ( ) : Promise < WatchListResponse > {
135+ return this . request < WatchListResponse > ( `/watch` , { headers : { Authorization : `Bearer ${ this . apiKey } ` } } )
136+ }
137+
138+ async watchDelete ( watchId : string ) : Promise < WatchDeleteResponse > {
139+ return this . request < WatchDeleteResponse > ( `/watch/${ watchId } ` , { method : 'DELETE' , headers : { Authorization : `Bearer ${ this . apiKey } ` } } )
140+ }
141+
142+ async watchPause ( watchId : string ) : Promise < WatchActionResponse > {
143+ return this . request < WatchActionResponse > ( `/watch/${ watchId } /pause` , { method : 'PATCH' , headers : this . headers ( ) } )
144+ }
145+
146+ async watchResume ( watchId : string ) : Promise < WatchActionResponse > {
147+ return this . request < WatchActionResponse > ( `/watch/${ watchId } /resume` , { method : 'PATCH' , headers : this . headers ( ) } )
148+ }
149+
150+ async watchCheck ( watchId : string ) : Promise < WatchActionResponse > {
151+ return this . request < WatchActionResponse > ( `/watch/${ watchId } /check` , { method : 'POST' , headers : this . headers ( ) } )
152+ }
153+ }
0 commit comments