Skip to content

Instantly share code, notes, and snippets.

@schickling
Created February 10, 2023 16:07
Show Gist options
  • Save schickling/ac21d25aaa601e1b334174cbf4bd7d6b to your computer and use it in GitHub Desktop.
Save schickling/ac21d25aaa601e1b334174cbf4bd7d6b to your computer and use it in GitHub Desktop.
import * as msgpack from 'msgpackr'
import { OT, pipe, T, Tagged } from './index.js'
export const fetchHead = (url: string | URL, headers?: HeadersInit): T.Effect<OT.HasTracer, FetchHeadError, Response> =>
pipe(
T.tryCatchPromise(
() => fetch(url as any, { method: 'head', headers }),
(error) => new FetchHeadError({ url, error }),
),
T.tap((res) => OT.addAttribute('http.status', res.status)),
OT.withSpan('fetchHead', { attributes: { 'http.url': url.toString() } }),
)
export const fetchText = (url: string | URL, headers?: HeadersInit): T.Effect<OT.HasTracer, FetchTextError, string> =>
pipe(
T.tryCatchPromise(
() => fetch(url as any, { headers }),
(error) => new FetchTextError({ url, error }),
),
T.tap((res) => OT.addAttribute('http.status', res.status)),
T.chain((resp) =>
resp.ok
? T.tryCatchPromise(
() => resp.text(),
(error) => new FetchTextError({ url, error, status: resp.status }),
)
: T.fail(new FetchTextError({ url, status: resp.status })),
),
OT.withSpan('fetchText', { attributes: { 'http.url': url.toString() } }),
)
// TODO refactor
export const fetchPostMsgpack = <TIn, TOut>({
url,
payload,
headers,
}: {
url: string | URL
payload: TIn
headers?: HeadersInit
}): T.Effect<OT.HasTracer, FetchMsgpackError, TOut> =>
pipe(
T.try(() => {
const formData = new FormData()
const payloadBuffer = msgpack.pack(payload)
const payloadBlob = new Blob([payloadBuffer], { type: 'application/octet-stream' })
formData.append('bytes', payloadBlob)
return formData
}),
T.chain((formData) => T.tryPromise(() => fetch(url as any, { headers, method: 'POST', body: formData }))),
T.mapError((error) => new FetchMsgpackError({ url, error })),
T.tap((res) => OT.addAttribute('http.status', res.status)),
T.chain((resp) =>
resp.ok
? T.tryCatchPromise(
() => resp.arrayBuffer(),
(error) => new FetchMsgpackError({ url, error, status: resp.status }),
)
: T.fail(new FetchMsgpackError({ url, status: resp.status })),
),
T.map((responseBuffer) => {
// NOTE this is needed since otherwise `new Date(someBigInt)` will throw
const packr = new msgpack.Packr({ useRecords: false, int64AsNumber: true })
return packr.unpack(new Uint8Array(responseBuffer)) as TOut
}),
OT.withSpan('fetchPostMsgpack', { attributes: { 'http.url': url.toString() } }),
)
export const fetchJSON = <T>(url: string | URL, headers?: HeadersInit): T.Effect<OT.HasTracer, FetchJSONError, T> =>
pipe(
T.tryCatchPromise(
() => fetch(url as any, { headers }),
(error) => new FetchJSONError({ url, error }),
),
T.tap((res) => OT.addAttribute('http.status', res.status)),
T.chain((resp) =>
resp.ok
? T.tryCatchPromise(
() => resp.json() as Promise<T>,
(error) => new FetchJSONError({ url, error, status: resp.status }),
)
: pipe(
T.tryCatchPromise(
() => resp.json(),
(error) => new FetchJSONError({ url, error, status: resp.status }),
),
T.chain((body) => T.fail(new FetchJSONError({ url, status: resp.status, body }))),
),
),
OT.withSpan('fetchJSON', { attributes: { 'http.url': url.toString() } }),
)
export const fetchJSONPost = <T>(url: string | URL, headers?: HeadersInit): T.Effect<OT.HasTracer, FetchJSONError, T> =>
pipe(
T.tryCatchPromise(
() => fetch(url as any, { headers, method: 'post' }),
(error) => new FetchJSONError({ url, error }),
),
T.tap((res) => OT.addAttribute('http.status', res.status)),
T.chain((resp) =>
resp.ok
? T.tryCatchPromise(
() => resp.json() as Promise<T>,
(error) => new FetchJSONError({ url, error, status: resp.status }),
)
: T.fail(new FetchJSONError({ url, status: resp.status })),
),
OT.withSpan('fetchJSONPost', { attributes: { 'http.url': url.toString() } }),
)
// TODO refactor to merge with `fetchJSONPost`
export const fetchJSONPostWithBody = <TOut, TIn>({
url,
body,
headers,
}: {
url: string | URL
body: TIn
headers?: HeadersInit
}): T.Effect<OT.HasTracer, FetchJSONError, TOut> =>
pipe(
T.tryCatchPromise(
() => fetch(url as any, { headers, method: 'post', body: JSON.stringify(body) }),
(error) => new FetchJSONError({ url, error }),
),
T.tap((res) => OT.addAttribute('http.status', res.status)),
T.chain((resp) =>
resp.ok
? T.tryCatchPromise(
() => resp.json() as Promise<TOut>,
(error) => new FetchJSONError({ url, error, status: resp.status }),
)
: pipe(
T.tryPromise(() => resp.json()),
T.mapError(() => new FetchJSONError({ url, status: resp.status })),
T.chain((jsonError) => T.fail(new FetchJSONError({ url, status: resp.status, error: jsonError }))),
),
),
OT.withSpan('fetchJSONPostWithBody', { attributes: { 'http.url': url.toString() } }),
)
export const fetchArrayBuffer = (
url: string | URL,
headers?: HeadersInit,
): T.Effect<OT.HasTracer, FetchArrayBufferError, ArrayBuffer> =>
pipe(
T.tryCatchPromise(
() => fetch(url, { headers }),
(error) => new FetchArrayBufferError({ url, error }),
),
T.tap((res) => OT.addAttribute('http.status', res.status)),
T.chain((resp) =>
resp.ok
? T.tryCatchPromise(
() => resp.arrayBuffer(),
(error) => new FetchArrayBufferError({ url, error, status: resp.status }),
)
: T.fail(new FetchArrayBufferError({ url, status: resp.status })),
),
OT.withSpan('fetchArrayBuffer', { attributes: { 'http.url': url.toString() } }),
)
export const fetchBlob = (url: string | URL): T.Effect<OT.HasTracer, FetchBlobError, Blob> =>
pipe(
T.tryCatchPromise(
() => fetch(url),
(error) => new FetchBlobError({ url, error }),
),
T.tap((res) => OT.addAttribute('http.status', res.status)),
T.chain((resp) =>
resp.ok
? T.tryCatchPromise(
() => resp.blob(),
(error) => new FetchBlobError({ url, error, status: resp.status }),
)
: T.fail(new FetchBlobError({ url, status: resp.status })),
),
OT.withSpan('fetchBlob', { attributes: { 'http.url': url.toString() } }),
)
export class FetchHeadError extends Tagged('FetchHeadError')<{
readonly url: string | URL
readonly error?: unknown
readonly status?: number
}> {
readonly message: string = `Couldn't fetch URL "${this.url}". ${this.error}`
toString = () => `FetchHeadError: ${JSON.stringify(this)}`
}
export class FetchTextError extends Tagged('FetchTextError')<{
readonly url: string | URL
readonly error?: unknown
readonly status?: number
}> {
readonly message: string = `Couldn't fetch URL "${this.url}". ${this.error}`
toString = () => `FetchTextError: ${JSON.stringify(this)}`
}
export class FetchMsgpackError extends Tagged('FetchMsgpackError')<{
readonly url: string | URL
readonly error?: unknown
readonly status?: number
}> {
readonly message: string = `Couldn't fetch URL "${this.url}". ${this.error}`
toString = () => `FetchMsgpackError: ${JSON.stringify(this)}`
}
export class FetchJSONError extends Tagged('FetchJSONError')<{
readonly url: string | URL
readonly error?: unknown
readonly status?: number
readonly body?: any
}> {
readonly message: string = `Couldn't fetch URL "${this.url}". ${this.error}`
toString = () => `FetchJSONError: ${JSON.stringify(this)}`
}
export class FetchArrayBufferError extends Tagged('FetchArrayBufferError')<{
readonly url: string | URL
readonly error?: unknown
readonly status?: number
}> {
readonly message: string = `Couldn't fetch URL "${this.url}". ${this.error}`
toString = () => `FetchArrayBufferError: ${JSON.stringify(this)}`
}
export class FetchBlobError extends Tagged('FetchBlobError')<{
readonly url: string | URL
readonly error?: unknown
readonly status?: number
}> {
readonly message: string = `Couldn't fetch URL "${this.url}". ${this.error}`
toString = () => `FetchBlobError: ${JSON.stringify(this)}`
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment