@typed/fp
Version:
Data Structures and Resources for fp-ts
162 lines (134 loc) • 4.57 kB
text/typescript
/**
* @typed/fp/browser is a place to place implementations of environment from other modules that require or
* are best used with implementations specificall for the browser.
* @since 0.9.4
*/
import * as Ei from 'fp-ts/Either'
import { Adapter } from './Adapter'
import * as D from './Disposable'
import { RafEnv, WhenIdleEnv } from './dom'
import * as E from './Env'
import * as http from './http'
import * as R from './Resume'
import * as S from './Stream'
/**
* @category Environment
* @since 0.9.4
*/
export const HttpEnv: http.HttpEnv = { http: E.fromResumeK(httpFetchRequest) }
function httpFetchRequest(
uri: string,
options: http.HttpOptions = {},
): R.Resume<Ei.Either<Error, http.HttpResponse>> {
return R.async((cb) => {
const { method = 'GET', headers = {}, body } = options
const disposable = D.settable()
const abortController = new AbortController()
disposable.addDisposable({
dispose: () => abortController.abort(),
})
const init: RequestInit = {
method,
headers: Object.entries(headers).map(([key, value = '']) => [key, value]),
body,
credentials: 'include',
signal: abortController.signal,
}
async function makeRequest() {
const response = await fetch(uri, init)
const headers: Record<string, string | undefined> = {}
response.headers.forEach((value, key) => {
headers[key] = value
})
const httpResponse: http.HttpResponse = {
status: response.status,
body: await response.json().catch(() => response.text()),
headers,
}
if (!disposable.isDisposed()) {
disposable.addDisposable(cb(Ei.right(httpResponse)))
}
}
makeRequest().catch((error) => {
if (!disposable.isDisposed()) {
disposable.addDisposable(cb(Ei.left(error)))
}
})
return disposable
})
}
/**
* Constructs an Adapter that utilizes a BroadcastChannel to communicate messages across
* all scripts of the same origin, including workers.
*
* _Note:_ An error will occur, and the stream will fail, if you send events which cannot be
* structurally cloned. See
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
* @category Constructor
* @since 0.12.2
*/
export const broadcastChannel = <A>(name: string): Adapter<A> => {
const channel = new BroadcastChannel(name)
const send = (event: A) => channel.postMessage(event)
const stream = S.newStream<A>((sink, scheduler) => {
const onMessage = (event: MessageEvent<A>) => sink.event(scheduler.currentTime(), event.data)
const onMessageError = (event: MessageEvent<Error>) =>
sink.error(scheduler.currentTime(), event.data)
channel.addEventListener('message', onMessage)
channel.addEventListener('messageerror', onMessageError)
return {
dispose: () => {
channel.removeEventListener('message', onMessage)
channel.removeEventListener('messageerror', onMessageError)
},
}
})
return [send, stream]
}
/**
* Utilize the Crypto API to generate 8-bit numbers
* @category Constructor
* @since 0.12.2
*/
export const random8Bits = (count: number): E.Of<readonly number[]> =>
E.fromIO(() => [...crypto.getRandomValues(new Uint8Array(count))] as const)
/**
* Utilize the Crypto API to generate 16-bit numbers
* @category Constructor
* @since 0.12.2
*/
export const random16Bits = (count: number): E.Of<readonly number[]> =>
E.fromIO(() => [...crypto.getRandomValues(new Uint16Array(count))] as const)
/**
* Utilize the Crypto API to generate 32-bit numbers
* @category Constructor
* @since 0.12.2
*/
export const random32Bits = (count: number): E.Of<readonly number[]> =>
E.fromIO(() => [...crypto.getRandomValues(new Uint32Array(count))] as const)
/**
* @category Environment
* @since 0.13.2
*/
export const rafEnv: RafEnv = {
raf: R.async((resume) => {
const disposable = D.settable()
let handle = requestAnimationFrame(
() => (handle = requestAnimationFrame((n) => disposable.addDisposable(resume(n)))),
)
disposable.addDisposable({ dispose: () => cancelAnimationFrame(handle) })
return disposable
}),
}
/**
* @category Environment
* @since 0.13.2
*/
export const whenIdleEnv: WhenIdleEnv = {
whenIdle: R.async((cb) => {
const disposable = D.settable()
const handle = window.requestIdleCallback((d) => disposable.addDisposable(cb(d)))
disposable.addDisposable({ dispose: () => cancelAnimationFrame(handle) })
return disposable
}),
}