Last active
November 2, 2023 12:41
-
-
Save zerkalica/422da76d96e74b255159e53bb2648229 to your computer and use it in GitHub Desktop.
fetch.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace $ { | |
export type $gd_rad_transport_req = Omit<RequestInit, 'headers'> & { | |
place?: string | |
deadline?: number | |
headers?: Record<string, string> | |
auth_disabled?: boolean | |
body_object?: object | |
} | |
const ostr = $mol_data_optional($mol_data_string) | |
export const $gd_kit_transport_error_response = $mol_data_record({ | |
code: ostr, | |
message: ostr | |
}) | |
export class $gd_kit_transport_cause extends $mol_object { | |
readonly res?: $mol_fetch_response | |
readonly client_message?: string | |
constructor( | |
readonly input: RequestInfo, | |
readonly init: $gd_rad_transport_req, | |
res_or_client_message?: $mol_fetch_response | string, | |
) { | |
super() | |
if (typeof res_or_client_message === 'string') { | |
this.client_message = res_or_client_message | |
this.res = undefined | |
} else { | |
this.client_message = undefined | |
this.res = res_or_client_message | |
} | |
} | |
} | |
export class $gd_kit_transport extends $mol_fetch { | |
@ $mol_mem | |
static url_base() { | |
return '' | |
} | |
static origin_default() { | |
const loc = this.$.$mol_dom_context.location | |
return loc.hostname === 'localhost' ? 'https://gd.ocas.ai' : loc.origin | |
} | |
static ws_origin() { | |
return this.origin().replace(/^http/, 'ws') | |
} | |
@ $mol_mem | |
static origin( next?: string ) { | |
return this.$.$mol_state_local.value( '$gd_kit_transport:origin', next ) ?? this.origin_default() | |
} | |
static url_prefix() { | |
return this.origin() + this.url_base() | |
} | |
static token_key() { | |
return 'kc_mol_at' | |
} | |
@ $mol_mem | |
static token( next? : string | null ) { | |
if (next) { | |
this.$.$mol_log3_rise({ | |
place: '$gd_kit_transport.token()', | |
message: 'set token', | |
token_part: next.substring(0, 5), | |
}) | |
} | |
return this.$.$mol_state_local.value(this.token_key(), next) ?? undefined | |
} | |
// custom auth headers | |
static headers_auth() { | |
const token = this.token() | |
if (! token) return undefined | |
return { | |
'Authorization': token | |
} | |
} | |
static headers_default(): Record<string, string> { | |
return { | |
'Content-Type': 'application/json', | |
} | |
} | |
static get(path: string, params?: $gd_rad_transport_req) { | |
return this.success(this.url_prefix() + path, { ...params, method: 'GET' }) | |
} | |
static head(path: string, params?: $gd_rad_transport_req) { | |
return this.success(this.url_prefix() + path, { ...params, method: 'HEAD' }) | |
} | |
// custom range headers | |
static range(path: string, raw?: $gd_rad_transport_req & { count_prefer?: 'exact' | 'planned' }) { | |
const { count_prefer, ...params } = raw ?? {} | |
const res = this.head(path, { | |
...params, | |
headers: { | |
...params?.headers, | |
'Range-Unit': 'items', | |
Prefer: `count=${count_prefer ?? 'exact'}`, | |
}, | |
}) | |
const headers = res.headers() | |
const range_str = headers.get('Content-Range') | |
const [all, from, to, total] = range_str?.match(/(?:(?:(\d+)\-(\d+))|(?:\*))\/((?:\d+)|(?:\*))$/) ?? [] | |
const count = ! total || total === '*' ? to : total | |
if (count === '*') return 0 | |
if (! count?.match(/^\d+$/)) { | |
this.$.$mol_log3_warn({ | |
place: '$gd_kit_transport.count()', | |
message: 'Cant get count of range', | |
hint: 'check backend' | |
}) | |
return undefined | |
} | |
return Number(count) | |
} | |
static post(path: string, params?: $gd_rad_transport_req) { | |
return this.success(this.url_prefix() + path, { ...params, method: 'POST' }) | |
} | |
static delete(path: string, params?: $gd_rad_transport_req) { | |
return this.success(this.url_prefix() + path, { ...params, method: 'DELETE' }) | |
} | |
@ $mol_mem | |
static auth_wait(next?: boolean) { | |
if (!this.token()) next = true | |
if (next !== undefined) { | |
this.$.$mol_log3_rise({ | |
place: '$gd_kit_transport.auth_promise()', | |
message: next ? 'awaiter' : 'null' | |
}) | |
} | |
if (! next) return false | |
const promise = $mol_promise<boolean>() | |
return Object.assign(promise, { destructor: () => promise.done(false) }) | |
} | |
static deadline() { | |
return 1000 | |
} | |
@ $mol_action | |
static override success(input: RequestInfo, params: $gd_rad_transport_req) { | |
let res: $mol_fetch_response | undefined | |
let init: $gd_rad_transport_req | undefined | |
do { | |
const headers_auth = this.headers_auth() | |
const headers: $gd_rad_transport_req['headers'] = { | |
... this.headers_default(), | |
'X-Requested-From': params?.place ?? '$gd_kit_transport.response_real()', | |
... params.headers, | |
} | |
if (! params.auth_disabled && headers_auth) Object.assign(headers, headers_auth) | |
const body = params.body ?? (params.body_object ? JSON.stringify(params.body_object) : undefined) | |
init = { ...params, body, headers } | |
res = this.response(input, init) | |
if( res.status() === 'success' ) return res | |
// if (res.code() === 401) // try refresh | |
if (this.auth_wait_need(res)) this.auth_wait(true) | |
} while ( this.auth_wait_need(res) ) | |
const cause = new this.$.$gd_kit_transport_cause(input, init, res) | |
throw new Error( this.message(cause), { cause } ) | |
} | |
protected static message( | |
{ res, input, init, client_message }: $gd_kit_transport_cause | |
) { | |
let details | |
let message | |
if (res) { | |
message = res.message() | |
const ctx = this.response_fail_object(res) | |
const obj = ctx?.object | |
if (obj?.message) details = this.error_details(obj) | |
if (!details && ctx?.text) details = ctx.text | |
} | |
const method = init.method === 'GET' ? undefined : init.method | |
return [ | |
client_message, | |
message, | |
details, | |
init?.place, | |
init ? `${method ? `${method} ` : ''} ${input}` : undefined, | |
].filter($mol_guard_defined).join(', ') | |
} | |
// custom error shape | |
protected static error_details(obj: ReturnType<typeof this.normalize_error>) { | |
return `${obj.message} [${obj.code || 'unk'}]` | |
} | |
// custom error shape | |
protected static normalize_error(json: unknown) { | |
return $gd_kit_transport_error_response(json as any) | |
} | |
protected static response_fail_object(res: $mol_fetch_response) { | |
let text | |
try { | |
text = res.text() // Do not use res.json() here | |
} catch (e) { | |
if ($mol_promise_like(e)) return $mol_fail_hidden(e) | |
this.$.$mol_log3_warn({ | |
place: '$gd_kit_transport_error.context()', | |
message: 'Can\'t read error text', | |
hint : 'Server must return valid text in error json response', | |
}) | |
return undefined | |
} | |
let json | |
try { | |
json = JSON.parse(text) | |
} catch (e) { | |
if ($mol_promise_like(e)) return $mol_fail_hidden(e) | |
return { text } | |
} | |
let object | |
try { | |
object = $gd_kit_transport_error_response(json as any) | |
} catch (e) { | |
if ($mol_promise_like(e)) return $mol_fail_hidden(e) | |
this.$.$mol_log3_warn({ | |
place: '$gd_kit_transport_error.context()', | |
message: 'Unknown response error object', | |
hint: 'Server must return known object in error json response', | |
}) | |
return { text } | |
} | |
return { text, object } | |
} | |
static override request(input: RequestInfo, init: $gd_rad_transport_req) { | |
const res = super.request(input, init) | |
const deadlined = Promise.race([ | |
new Promise<Awaited<typeof res>>( | |
(res, rej) => setTimeout(() => { | |
const cause = new this.$.$gd_kit_transport_cause(input, init, 'Client deadline exceeded') | |
const message = this.message(cause) | |
rej( new Error( message, { cause } ) ) | |
}, init.deadline ?? this.deadline()) | |
), | |
res | |
]) | |
return Object.assign(deadlined, | |
{ destructor: () => res.destructor() } | |
) | |
} | |
protected static auth_wait_need(res: $mol_fetch_response) { | |
const code = res.code() | |
return code === 401 || code === 403 | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment