Skip to content

Instantly share code, notes, and snippets.

@punmechanic
Created May 15, 2019 13:05
Show Gist options
  • Save punmechanic/797d334e0728842dcad064925e28748b to your computer and use it in GitHub Desktop.
Save punmechanic/797d334e0728842dcad064925e28748b to your computer and use it in GitHub Desktop.
awful effect-based HTTP. one benefit this provides is that it is more exhaustive than using a functional approach because it uses different 'Response' types. But it is quite verbose and would need more work to be a serious solution.
// TODO: This does not work in mock mode.
import { HTTPAPI, NotFoundError } from "../api"
import React from "react"
type Maybe<T> = T | undefined
export interface Request<T> {
then<T2>(f: (data: Maybe<T>) => Maybe<T2>): Request<Maybe<T2>>
run(): Promise<Response<Maybe<T>>>
}
class PendingResponse<T> implements Response<Maybe<T>> {
map<T2>(): Response<T2> {
return new PendingResponse<T2>()
}
orElse<T2>(f: () => T2): T2 {
return f()
}
}
export interface Response<T> {
map<T2>(f: (data: Maybe<T>) => T2): Response<T2>
orElse<T2>(f: () => T2): T | T2
}
class OkResponse<T> implements Response<T> {
readonly data: T
constructor(data: T) {
this.data = data
}
map<T2>(f: (data: Maybe<T>) => T2): Response<T2> {
return new OkResponse(f(this.data))
}
orElse(): T {
return this.data
}
}
export function isPending<T>(response: Response<T>): boolean {
return response instanceof PendingResponse
}
export function pending<T>(): Response<Maybe<T>> {
return new PendingResponse<T>()
}
class TransformedRequest<TOriginal, TNext> implements Request<Maybe<TNext>> {
readonly original: Request<TOriginal>
private readonly f: (data: Maybe<TOriginal>) => Maybe<TNext>
constructor(
original: Request<TOriginal>,
f: (data: Maybe<TOriginal>) => Maybe<TNext>
) {
this.original = original
this.f = f
}
then<TNext2>(
f: (data: Maybe<TNext>) => Maybe<TNext2>
): Request<Maybe<TNext2>> {
return new TransformedRequest(this, f)
}
run(): Promise<Response<Maybe<TNext>>> {
return this.original.run().then(r => r.map(this.f))
}
}
type HttpMethod = "get" | "delete"
export function useEffectualHttp<T>(
req: Request<T>,
watch: unknown[]
): Response<Maybe<T>> {
const [response, setResponse] = React.useState(pending<T>())
React.useEffect(() => {
req
.run()
.then(setResponse)
.catch(setResponse)
}, [...watch])
return response
}
class EffectualRequest<T> implements Request<T> {
readonly method: HttpMethod
readonly url: string
constructor(method: HttpMethod, url: string) {
this.method = method
this.url = url
}
then<T2>(f: (data: Maybe<T>) => Maybe<T2>): Request<Maybe<T2>> {
return new TransformedRequest<Maybe<T>, Maybe<T2>>(this, f)
}
run(): Promise<Response<Maybe<T>>> {
const api = new HTTPAPI()
const f = api[this.method] as (url: string) => Promise<T>
// Make sure to preserve 'this'
return f
.call(api, this.url)
.then(data => new OkResponse(data))
.catch(e => {
if (e instanceof NotFoundError) {
return new OkResponse<Maybe<T>>(undefined)
}
return Promise.reject(e)
})
}
}
export default class EffectualAPI {
get<T = unknown>(url: string): Request<T> {
return new EffectualRequest<T>("get", url)
}
delete<T = void>(url: string): Request<T> {
return new EffectualRequest<T>("delete", url)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment