Skip to content

Instantly share code, notes, and snippets.

@schickling
Created August 3, 2023 12:51
Show Gist options
  • Save schickling/7531326455a217f25362cff2a37b20c5 to your computer and use it in GitHub Desktop.
Save schickling/7531326455a217f25362cff2a37b20c5 to your computer and use it in GitHub Desktop.
import * as Data from '@effect/data/Data'
import { pipe } from '@effect/data/Function'
import * as Effect from '@effect/io/Effect'
import * as msgpack from 'msgpackr'
import * as Otel from './Otel/index.js'
export const fetchHead = (
url: string | URL,
headers?: HeadersInit,
): Effect.Effect<Otel.Tracer, FetchHeadError, Response> =>
pipe(
Effect.tryPromise({
try: () => fetch(url as any, { method: 'head', headers }),
catch: (error) => new FetchHeadError({ url, error }),
}),
Effect.tap((res) => Otel.addAttribute('http.status', res.status)),
Otel.withSpan('fetchHead', { attributes: { 'http.url': url.toString() } }),
)
export const fetchText = (
url: string | URL,
headers?: HeadersInit,
): Effect.Effect<Otel.Tracer, FetchTextError, string> =>
pipe(
Effect.tryPromise({
try: () => fetch(url as any, { headers }),
catch: (error) => new FetchTextError({ url, error }),
}),
Effect.tap((res) => Otel.addAttribute('http.status', res.status)),
Effect.flatMap((resp) =>
resp.ok
? Effect.tryPromise({
try: () => resp.text(),
catch: (error) => new FetchTextError({ url, error, status: resp.status }),
})
: Effect.fail(new FetchTextError({ url, status: resp.status })),
),
Otel.withSpan('fetchText', { attributes: { 'http.url': url.toString() } }),
)
// TODO refactor
export const fetchPostMsgpack = <TIn, TOut>({
url,
payload,
headers,
}: {
url: string | URL
payload: TIn
headers?: HeadersInit
}): Effect.Effect<Otel.Tracer, FetchMsgpackError, TOut> =>
pipe(
Effect.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
}),
Effect.flatMap((formData) =>
Effect.tryPromise(() => fetch(url as any, { headers, method: 'POST', body: formData })),
),
Effect.mapError((error) => new FetchMsgpackError({ url, error })),
Effect.tap((res) => Otel.addAttribute('http.status', res.status)),
Effect.flatMap((resp) =>
resp.ok
? Effect.tryPromise({
try: () => resp.arrayBuffer(),
catch: (error) => new FetchMsgpackError({ url, error, status: resp.status }),
})
: Effect.fail(new FetchMsgpackError({ url, status: resp.status })),
),
Effect.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
}),
Otel.withSpan('fetchPostMsgpack', { attributes: { 'http.url': url.toString() } }),
)
export const fetchJSON = <T>(url: string | URL, headers?: HeadersInit): Effect.Effect<Otel.Tracer, FetchJSONError, T> =>
pipe(
Effect.tryPromise({
try: () => fetch(url as any, { headers }),
catch: (error) => new FetchJSONError({ url, error }),
}),
Effect.tap((res) => Otel.addAttribute('http.status', res.status)),
Effect.flatMap((resp) =>
resp.ok
? Effect.tryPromise({
try: () => resp.json() as Promise<T>,
catch: (error) =>
new FetchJSONError({
url,
error,
status: resp.status,
headers: Object.fromEntries(resp.headers.entries()),
}),
})
: pipe(
Effect.tryPromise({
try: () => resp.json(),
catch: (error) =>
new FetchJSONError({
url,
error,
status: resp.status,
headers: Object.fromEntries(resp.headers.entries()),
}),
}),
Effect.flatMap((body) =>
Effect.fail(
new FetchJSONError({
url,
status: resp.status,
body,
headers: Object.fromEntries(resp.headers.entries()),
}),
),
),
),
),
Otel.withSpan('fetchJSON', { attributes: { 'http.url': url.toString() } }),
)
export const fetchJSONPost = <T>(
url: string | URL,
headers?: HeadersInit,
): Effect.Effect<Otel.Tracer, FetchJSONError, T> =>
pipe(
Effect.tryPromise({
try: () => fetch(url as any, { headers, method: 'post' }),
catch: (error) => new FetchJSONError({ url, error }),
}),
Effect.tap((res) => Otel.addAttribute('http.status', res.status)),
Effect.flatMap((resp) =>
resp.ok
? Effect.tryPromise({
try: () => resp.json() as Promise<T>,
catch: (error) => new FetchJSONError({ url, error, status: resp.status }),
})
: Effect.fail(new FetchJSONError({ url, status: resp.status })),
),
Otel.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
}): Effect.Effect<Otel.Tracer, FetchJSONError, TOut> =>
pipe(
Effect.tryPromise({
try: () => fetch(url as any, { headers, method: 'post', body: JSON.stringify(body) }),
catch: (error) => new FetchJSONError({ url, error }),
}),
Effect.tap((res) => Otel.addAttribute('http.status', res.status)),
Effect.flatMap((resp) =>
resp.ok
? Effect.tryPromise({
try: () => resp.json() as Promise<TOut>,
catch: (error) => new FetchJSONError({ url, error, status: resp.status }),
})
: pipe(
Effect.tryPromise(() => resp.json()),
Effect.mapError(() => new FetchJSONError({ url, status: resp.status })),
Effect.flatMap((jsonError) =>
Effect.fail(new FetchJSONError({ url, status: resp.status, error: jsonError })),
),
),
),
Otel.withSpan('fetchJSONPostWithBody', { attributes: { 'http.url': url.toString() } }),
)
export const fetchArrayBuffer = (
url: string | URL,
headers?: HeadersInit,
): Effect.Effect<Otel.Tracer, FetchArrayBufferError, ArrayBuffer> =>
pipe(
Effect.tryPromise({
try: () => fetch(url, { headers }),
catch: (error) => new FetchArrayBufferError({ url, error }),
}),
Effect.tap((res) => Otel.addAttribute('http.status', res.status)),
Effect.flatMap((resp) =>
resp.ok
? Effect.tryPromise({
try: () => resp.arrayBuffer(),
catch: (error) => new FetchArrayBufferError({ url, error, status: resp.status }),
})
: Effect.fail(new FetchArrayBufferError({ url, status: resp.status })),
),
Otel.withSpan('fetchArrayBuffer', { attributes: { 'http.url': url.toString() } }),
)
export const fetchBlob = (url: string | URL): Effect.Effect<Otel.Tracer, FetchBlobError, Blob> =>
pipe(
Effect.tryPromise({
try: () => fetch(url),
catch: (error) => new FetchBlobError({ url, error }),
}),
Effect.tap((res) => Otel.addAttribute('http.status', res.status)),
Effect.flatMap((resp) =>
resp.ok
? Effect.tryPromise({
try: () => resp.blob(),
catch: (error) => new FetchBlobError({ url, error, status: resp.status }),
})
: Effect.fail(new FetchBlobError({ url, status: resp.status })),
),
Otel.withSpan('fetchBlob', { attributes: { 'http.url': url.toString() } }),
)
export class FetchHeadError extends Data.TaggedClass('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 Data.TaggedClass('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 Data.TaggedClass('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 Data.TaggedClass('FetchJSONError')<{
readonly url: string | URL
readonly error?: unknown
readonly status?: number
readonly body?: any
readonly headers?: Record<string, string>
}> {
readonly message: string = `Couldn't fetch URL "${this.url}". ${this.error}`
toString = () => `FetchJSONError: ${JSON.stringify(this)}`
}
export class FetchArrayBufferError extends Data.TaggedClass('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 Data.TaggedClass('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