Created
August 8, 2023 08:35
-
-
Save schickling/785ad9c14fdd2187a235ce19c96686e0 to your computer and use it in GitHub Desktop.
This file contains 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
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