Skip to content

Instantly share code, notes, and snippets.

@zerkalica
Last active November 2, 2023 12:41
Show Gist options
  • Save zerkalica/422da76d96e74b255159e53bb2648229 to your computer and use it in GitHub Desktop.
Save zerkalica/422da76d96e74b255159e53bb2648229 to your computer and use it in GitHub Desktop.
fetch.ts
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