effect-app 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/_cjs/client/clientFor.cjs +147 -96
  3. package/_cjs/client/clientFor.cjs.map +1 -1
  4. package/_cjs/client.cjs +0 -11
  5. package/_cjs/client.cjs.map +1 -1
  6. package/dist/client/clientFor.d.ts +38 -28
  7. package/dist/client/clientFor.d.ts.map +1 -1
  8. package/dist/client/clientFor.js +115 -97
  9. package/dist/client.d.ts +0 -1
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +1 -2
  12. package/package.json +2 -32
  13. package/src/client/clientFor.ts +241 -158
  14. package/src/client.ts +0 -1
  15. package/vitest.config.ts.timestamp-1670862388823.mjs +23 -0
  16. package/_cjs/client/clientFor2.cjs +0 -197
  17. package/_cjs/client/clientFor2.cjs.map +0 -1
  18. package/_cjs/client/fetch.cjs +0 -176
  19. package/_cjs/client/fetch.cjs.map +0 -1
  20. package/_cjs/client/router.cjs +0 -31
  21. package/_cjs/client/router.cjs.map +0 -1
  22. package/dist/client/clientFor2.d.ts +0 -49
  23. package/dist/client/clientFor2.d.ts.map +0 -1
  24. package/dist/client/clientFor2.js +0 -165
  25. package/dist/client/fetch.d.ts +0 -56
  26. package/dist/client/fetch.d.ts.map +0 -1
  27. package/dist/client/fetch.js +0 -158
  28. package/dist/client/router.d.ts +0 -32
  29. package/dist/client/router.d.ts.map +0 -1
  30. package/dist/client/router.js +0 -17
  31. package/src/client/clientFor2.ts +0 -339
  32. package/src/client/fetch.ts +0 -299
  33. package/src/client/router.ts +0 -87
  34. package/vitest.config.ts.timestamp-1709838418683-4e8d39caec6be.mjs +0 -33
  35. package/vitest.config.ts.timestamp-1711656440837-b61cd88636759.mjs +0 -37
  36. package/vitest.config.ts.timestamp-1711724061890-f88ec51585e5c.mjs +0 -37
  37. package/vitest.config.ts.timestamp-1711743471020-0a3f182bca84f.mjs +0 -0
  38. package/vitest.config.ts.timestamp-1711743489536-f03d3db08b368.mjs +0 -37
  39. package/vitest.config.ts.timestamp-1711743593445-f537ce2bb2c6a.mjs +0 -0
@@ -1,339 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
-
4
- import { Effect, flow, HashMap, Layer, Option, Predicate, Struct } from "@effect-app/core"
5
- import { RpcResolver } from "@effect/rpc"
6
- import { HttpRpcResolver } from "@effect/rpc-http"
7
- import type { RpcRouter } from "@effect/rpc/RpcRouter"
8
- import type * as Serializable from "@effect/schema/Serializable"
9
- import { S } from "effect-app"
10
- import type { FetchResponse } from "effect-app/client"
11
- import { ApiConfig, makePathWithBody, makePathWithQuery } from "effect-app/client"
12
- import { HttpClient, HttpClientRequest } from "effect-app/http"
13
- import type { Schema } from "effect-app/schema"
14
- import { typedKeysOf } from "effect-app/utils"
15
- import type * as Request from "effect/Request"
16
- import { Path } from "path-parser"
17
-
18
- type Requests = Record<string, any>
19
-
20
- const apiClient = Effect.gen(function*() {
21
- const client = yield* HttpClient.HttpClient
22
- const config = yield* ApiConfig.Tag
23
- return client.pipe(
24
- HttpClient.mapRequest(HttpClientRequest.prependUrl(config.apiUrl + "/rpc")),
25
- HttpClient.mapRequest(
26
- HttpClientRequest.setHeaders(config.headers.pipe(Option.getOrElse(() => HashMap.empty())))
27
- )
28
- )
29
- })
30
-
31
- export type Client<M extends Requests> =
32
- & RequestHandlers<
33
- ApiConfig | HttpClient.HttpClient.Service,
34
- never, // SupportedErrors | FetchError | ResError,
35
- M
36
- >
37
- & RequestHandlersE<
38
- ApiConfig | HttpClient.HttpClient.Service,
39
- never, // SupportedErrors | FetchError | ResError,
40
- M
41
- >
42
-
43
- export function clientFor2(layers: Layer.Layer<never, never, never>) {
44
- const cache = new Map<any, Client<any>>()
45
-
46
- return <M extends Requests>(
47
- models: M
48
- ): Client<Omit<M, "meta">> => {
49
- const found = cache.get(models)
50
- if (found) {
51
- return found
52
- }
53
- const m = clientFor_(models, layers)
54
- cache.set(models, m)
55
- return m
56
- }
57
- }
58
-
59
- type Req = S.Schema.All & {
60
- new(...args: any[]): any
61
- _tag: string
62
- fields: S.Struct.Fields
63
- success: S.Schema.Any
64
- failure: S.Schema.Any
65
- config?: Record<string, any>
66
- }
67
-
68
- function clientFor_<M extends Requests>(models: M, layers = Layer.empty) {
69
- type Filtered = {
70
- [K in keyof Requests as Requests[K] extends Req ? K : never]: Requests[K] extends Req ? Requests[K] : never
71
- }
72
- const filtered = typedKeysOf(models).reduce((acc, cur) => {
73
- if (
74
- Predicate.isObject(models[cur])
75
- && (models[cur].success)
76
- ) {
77
- acc[cur as keyof Filtered] = models[cur]
78
- }
79
- return acc
80
- }, {} as Record<keyof Filtered, Req>)
81
-
82
- const meta = (models as any).meta as { moduleName: string }
83
- if (!meta) throw new Error("No meta defined in Resource!")
84
-
85
- const resolver = flow(
86
- HttpRpcResolver.make<RpcRouter<any, any>>,
87
- (_) => RpcResolver.toClient(_ as any)
88
- )
89
-
90
- const baseClient = apiClient.pipe(
91
- Effect.andThen(HttpClient.mapRequest(HttpClientRequest.appendUrl("/" + meta.moduleName)))
92
- )
93
-
94
- return (typedKeysOf(filtered)
95
- .reduce((prev, cur) => {
96
- const h = filtered[cur]!
97
-
98
- const Request = h
99
- const Response = h.success
100
-
101
- const encodeRequest = S.encodeSync(
102
- Request as unknown as S.Schema<any, any>
103
- )
104
-
105
- const requestName = `${meta.moduleName}.${cur as string}`
106
- .replaceAll(".js", "")
107
-
108
- const requestMeta = {
109
- method: "POST", // TODO
110
- Request,
111
- Response,
112
- mapPath: requestName,
113
- name: requestName
114
- }
115
-
116
- const client = baseClient.pipe(
117
- Effect.andThen(HttpClient.mapRequest(HttpClientRequest.appendUrlParam("action", cur as string))),
118
- Effect.andThen(resolver)
119
- )
120
-
121
- const fields = Struct.omit(Request.fields, "_tag")
122
- const p = requestName
123
- const path = new Path(p) // TODO
124
- // @ts-expect-error doc
125
- prev[cur] = requestMeta.method === "GET"
126
- ? Object.keys(fields).length === 0
127
- ? {
128
- handler: client
129
- .pipe(
130
- Effect.andThen((cl) => cl(new Request())),
131
- Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO
132
- Effect
133
- .withSpan("client.request " + requestName, {
134
- captureStackTrace: false,
135
- attributes: { "request.name": requestName }
136
- }),
137
- Effect.provide(layers)
138
- ),
139
- ...requestMeta
140
- }
141
- : {
142
- handler: (req: any) =>
143
- client
144
- .pipe(
145
- Effect.andThen((cl) => cl(new Request(req))),
146
- Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO
147
- Effect
148
- .withSpan("client.request " + requestName, {
149
- captureStackTrace: false,
150
- attributes: { "request.name": requestName }
151
- }),
152
- Effect.provide(layers)
153
- ),
154
- ...requestMeta,
155
- mapPath: (req: any) => req ? makePathWithQuery(path, encodeRequest(req)) : p
156
- }
157
- : Object.keys(fields).length === 0
158
- ? {
159
- handler: client
160
- .pipe(
161
- Effect.andThen((cl) => cl(new Request())),
162
- Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO
163
- Effect.withSpan("client.request " + requestName, {
164
- captureStackTrace: false,
165
- attributes: { "request.name": requestName }
166
- }),
167
- Effect.provide(layers)
168
- ),
169
- ...requestMeta
170
- }
171
- : {
172
- handler: (req: any) =>
173
- client
174
- .pipe(
175
- Effect.andThen((cl) => cl(new Request(req))),
176
- Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO
177
- Effect.withSpan("client.request " + requestName, {
178
- captureStackTrace: false,
179
- attributes: { "request.name": requestName }
180
- }),
181
- Effect.provide(layers)
182
- ),
183
-
184
- ...requestMeta,
185
- mapPath: (req: any) =>
186
- req
187
- ? requestMeta.method === "DELETE"
188
- ? makePathWithQuery(path, encodeRequest(req))
189
- : makePathWithBody(path, encodeRequest(req))
190
- : p
191
- }
192
-
193
- // generate handler
194
-
195
- // @ts-expect-error doc
196
- prev[`${cur}E`] = requestMeta.method === "GET"
197
- ? Object.keys(fields).length === 0
198
- ? {
199
- handler: client
200
- .pipe(
201
- Effect.andThen((cl) => cl(new Request())),
202
- Effect.flatMap((res) => S.encode(Response)(res)), // TODO
203
- Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO,
204
- Effect
205
- .withSpan("client.request " + requestName, {
206
- captureStackTrace: false,
207
- attributes: { "request.name": requestName }
208
- }),
209
- Effect.provide(layers)
210
- ),
211
- ...requestMeta
212
- }
213
- : {
214
- handler: (req: any) =>
215
- client
216
- .pipe(
217
- Effect.andThen((cl) => cl(new Request(req))),
218
- Effect.flatMap((res) => S.encode(Response)(res)), // TODO
219
- Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO,
220
- Effect
221
- .withSpan("client.request " + requestName, {
222
- captureStackTrace: false,
223
- attributes: { "request.name": requestName }
224
- }),
225
- Effect.provide(layers)
226
- ),
227
-
228
- ...requestMeta,
229
- mapPath: (req: any) => req ? makePathWithQuery(path, encodeRequest(req)) : p
230
- }
231
- : Object.keys(fields).length === 0
232
- ? {
233
- handler: client
234
- .pipe(
235
- Effect.andThen((cl) => cl(new Request())),
236
- Effect.flatMap((res) => S.encode(Response)(res)), // TODO
237
- Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO,
238
- Effect.withSpan("client.request " + requestName, {
239
- captureStackTrace: false,
240
- attributes: { "request.name": requestName }
241
- }),
242
- Effect.provide(layers)
243
- ),
244
- ...requestMeta
245
- }
246
- : {
247
- handler: (req: any) =>
248
- client
249
- .pipe(
250
- Effect.andThen((cl) => cl(new Request(req))),
251
- Effect.flatMap((res) => S.encode(Response)(res)), // TODO
252
- Effect.map((_) => ({ body: _, status: 200, headers: {} })), // TODO,
253
- Effect.withSpan("client.request " + requestName, {
254
- captureStackTrace: false,
255
- attributes: { "request.name": requestName }
256
- }),
257
- Effect.provide(layers)
258
- ),
259
-
260
- ...requestMeta,
261
- mapPath: (req: any) =>
262
- req
263
- ? requestMeta.method === "DELETE"
264
- ? makePathWithQuery(path, encodeRequest(req))
265
- : makePathWithBody(path, encodeRequest(req))
266
- : p
267
- }
268
- // generate handler
269
-
270
- return prev
271
- }, {} as Client<M>))
272
- }
273
-
274
- export type ExtractResponse<T> = T extends Schema<any, any, any> ? Schema.Type<T>
275
- : T extends unknown ? void
276
- : never
277
-
278
- export type ExtractEResponse<T> = T extends Schema<any, any, any> ? Schema.Encoded<T>
279
- : T extends unknown ? void
280
- : never
281
-
282
- type IsEmpty<T> = keyof T extends never ? true
283
- : false
284
-
285
- type Cruft = "_tag" | Request.RequestTypeId | typeof Serializable.symbol | typeof Serializable.symbolResult
286
-
287
- // TODO: refactor to new Request pattern, then filter out non-requests similar to the runtime changes in clientFor, and matchFor (boilerplate)
288
- type RequestHandlers<R, E, M extends Requests> = {
289
- [K in keyof M]: IsEmpty<Omit<S.Schema.Type<M[K]>, Cruft>> extends true ? {
290
- handler: Effect<FetchResponse<Schema.Type<M[K]["success"]>>, Schema.Type<M[K]["failure"]> | E, R>
291
- Request: M[K]
292
- Reponse: Schema.Type<M[K]["success"]>
293
- mapPath: string
294
- name: string
295
- }
296
- : {
297
- handler: (
298
- req: Omit<S.Schema.Type<M[K]>, Cruft>
299
- ) => Effect<
300
- FetchResponse<Schema.Type<M[K]["success"]>>,
301
- Schema.Type<M[K]["failure"]> | E,
302
- R
303
- >
304
- Request: M[K]
305
- Reponse: Schema.Type<M[K]["success"]>
306
- mapPath: (req: Omit<S.Schema.Type<M[K]>, Cruft>) => string
307
- name: string
308
- }
309
- }
310
-
311
- type RequestHandlersE<R, E, M extends Requests> = {
312
- [K in keyof M & string as `${K}E`]: IsEmpty<Omit<S.Schema.Type<M[K]>, Cruft>> extends true ? {
313
- handler: Effect<
314
- FetchResponse<Schema.Encoded<M[K]["success"]>>,
315
- Schema.Type<M[K]["failure"]> | E,
316
- R
317
- >
318
- Request: M[K]
319
- Reponse: Schema.Type<M[K]["success"]>
320
- mapPath: string
321
- name: string
322
- }
323
- : {
324
- handler: (
325
- req: Omit<
326
- S.Schema.Type<M[K]>,
327
- Cruft
328
- >
329
- ) => Effect<
330
- FetchResponse<Schema.Encoded<M[K]["success"]>>,
331
- Schema.Type<M[K]["failure"]> | E,
332
- R
333
- >
334
- Request: M[K]
335
- Reponse: Schema.Type<M[K]["success"]>
336
- mapPath: (req: Omit<S.Schema.Type<M[K]>, Cruft>) => string
337
- name: string
338
- }
339
- }
@@ -1,299 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { Effect, HashMap, Option } from "@effect-app/core"
3
- import { constant } from "@effect-app/core/Function"
4
- import type { Headers, HttpError, HttpRequestError, HttpResponseError, Method } from "@effect-app/core/http/http-client"
5
- import type { ResponseError } from "@effect/platform/HttpClientError"
6
- import { Record } from "effect"
7
- import type { REST, Schema } from "effect-app/schema"
8
- import { StringId } from "effect-app/schema"
9
- import { Path } from "path-parser"
10
- import qs from "query-string"
11
- import { HttpClient, HttpClientRequest } from "../http.js"
12
- import { S } from "../lib.js"
13
- import { PreludeLogger } from "../logger.js"
14
- import { ApiConfig } from "./config.js"
15
- import type { SupportedErrors } from "./errors.js"
16
- import {
17
- InvalidStateError,
18
- NotFoundError,
19
- NotLoggedInError,
20
- OptimisticConcurrencyException,
21
- ServiceUnavailableError,
22
- UnauthorizedError,
23
- ValidationError
24
- } from "./errors.js"
25
-
26
- export type FetchError = HttpError<string>
27
-
28
- export class ResError {
29
- public readonly _tag = "ResponseError"
30
- constructor(public readonly error: unknown) {}
31
- }
32
-
33
- const getClient = Effect.flatMap(
34
- HttpClient.HttpClient,
35
- (defaultClient) =>
36
- Effect.map(ApiConfig.Tag, ({ apiUrl, headers }) =>
37
- defaultClient
38
- .pipe(
39
- HttpClient.filterStatusOk,
40
- HttpClient
41
- .mapRequest((_) =>
42
- _.pipe(
43
- HttpClientRequest.acceptJson,
44
- HttpClientRequest.prependUrl(apiUrl),
45
- HttpClientRequest
46
- .setHeaders({
47
- "request-id": Option.getOrUndefined(Option.flatMap(headers, (_) => HashMap.get(_, "request-id")))
48
- ?? StringId.make(),
49
- ...Option.getOrUndefined(Option.map(headers, (_) => Object.fromEntries(_)))
50
- })
51
- )
52
- ),
53
- HttpClient
54
- .tapRequest((r) =>
55
- PreludeLogger
56
- .logDebug(`[HTTP] ${r.method}`)
57
- .pipe(Effect.annotateLogs({
58
- "url": r.url,
59
- "body": r.body._tag === "Uint8Array"
60
- ? new TextDecoder().decode(r.body.body)
61
- : r.body._tag,
62
- "headers": r.headers
63
- }))
64
- ),
65
- HttpClient
66
- .mapEffect((_) =>
67
- (_.status === 204
68
- ? Effect.sync(() => ({ status: _.status, body: void 0, headers: _.headers }))
69
- : Effect.map(_.json, (body) => ({ status: _.status, body, headers: _.headers })))
70
- .pipe(Effect.withSpan("client.response", { captureStackTrace: false }))
71
- )
72
- ))
73
- )
74
-
75
- export function fetchApi(
76
- method: Method,
77
- path: string,
78
- body?: unknown,
79
- responseError?: S.Schema.AnyNoContext
80
- ) {
81
- return Effect.flatMap(getClient, (client) =>
82
- (method === "GET"
83
- ? client.execute(HttpClientRequest.make(method)(path))
84
- : body === undefined
85
- ? client.execute(HttpClientRequest.make(method)(path))
86
- : HttpClientRequest
87
- .make(method)(path)
88
- .pipe(HttpClientRequest.bodyJson(body), Effect.flatMap(client.execute)))
89
- .pipe(
90
- Effect
91
- .catchTag(
92
- "ResponseError",
93
- (err): Effect<FetchResponse<unknown>, ResponseError | SupportedErrors> => {
94
- const toError = <R, From, To>(s: Schema<To, From, R>) =>
95
- Effect
96
- .flatMap(
97
- err
98
- .response
99
- .json,
100
- (_) => S.decodeUnknown(s)(_).pipe(Effect.catchAll(() => Effect.fail(err)))
101
- )
102
- .pipe(Effect.flatMap(Effect.fail))
103
-
104
- // opposite of api's `defaultErrorHandler`
105
- if (err.response.status === 404) {
106
- return toError(NotFoundError)
107
- }
108
- if (err.response.status === 400) {
109
- return toError(ValidationError)
110
- }
111
- if (err.response.status === 401) {
112
- return toError(NotLoggedInError)
113
- }
114
- // TODO: DomainError
115
- if (err.response.status === 422) {
116
- return responseError
117
- ? toError(S.Union(responseError, InvalidStateError) as any)
118
- : toError(InvalidStateError)
119
- }
120
- if (err.response.status === 503) {
121
- return toError(ServiceUnavailableError)
122
- }
123
- if (err.response.status === 403) {
124
- return toError(UnauthorizedError)
125
- }
126
- if (err.response.status === 412) {
127
- return toError(OptimisticConcurrencyException)
128
- }
129
- return Effect.fail(err)
130
- }
131
- ),
132
- Effect.catchTags({
133
- "ResponseError": (err) =>
134
- Effect
135
- .orDie(
136
- err
137
- .response
138
- .text
139
- // TODO
140
- )
141
- .pipe(Effect
142
- .flatMap((_) =>
143
- Effect.fail({
144
- _tag: "HttpErrorResponse" as const,
145
- response: {
146
- body: Option.fromNullable(_),
147
- status: err.response.status,
148
- headers: err.response.headers
149
- }
150
- } as HttpResponseError<unknown>)
151
- )),
152
- "RequestError": (err) => Effect.fail({ _tag: "HttpErrorRequest", error: err.cause } as HttpRequestError)
153
- }),
154
- Effect.scoped
155
- ))
156
- }
157
-
158
- export function fetchApi2S<
159
- RequestR,
160
- RequestFrom,
161
- RequestTo,
162
- ResponseR,
163
- ResponseFrom,
164
- ResponseTo,
165
- ResponseErrorFrom = never,
166
- ResponseErrorTo extends { _tag: string } = never
167
- >(
168
- request: Schema<RequestTo, RequestFrom, RequestR>,
169
- response: Schema<ResponseTo, ResponseFrom, ResponseR>,
170
- responseError?: Schema<ResponseErrorTo, ResponseErrorFrom, never>
171
- ) {
172
- const encodeRequest = S.encode(request)
173
- const decRes = S.decodeUnknown(response)
174
- const decodeRes = (u: unknown) => Effect.mapError(decRes(u), (err) => new ResError(err))
175
- const parse = mapResponseM(decodeRes)
176
- return (method: Method, path: Path) => (req: RequestTo) => {
177
- return Effect.andThen(encodeRequest(req), (encoded) =>
178
- fetchApi(
179
- method,
180
- method === "DELETE"
181
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
182
- ? makePathWithQuery(path, encoded as any)
183
- : makePathWithBody(path, encoded as any),
184
- encoded,
185
- responseError
186
- )
187
- .pipe(Effect.flatMap(parse)))
188
- }
189
- }
190
-
191
- export function fetchApi3S<RequestA, RequestE, ResponseE = unknown, ResponseA = void>({
192
- Request,
193
- Response
194
- }: {
195
- // eslint-disable-next-line @typescript-eslint/ban-types
196
- Request: REST.RequestSchemed<RequestA, RequestE>
197
- // eslint-disable-next-line @typescript-eslint/ban-types
198
- Response: REST.ReqRes<ResponseA, ResponseE, any>
199
- }) {
200
- return fetchApi2S(Request, Response, Request.errors)(
201
- Request.method,
202
- new Path(Request.path)
203
- )
204
- }
205
-
206
- export function fetchApi3SE<RequestA, RequestE, ResponseE = unknown, ResponseA = void>({
207
- Request,
208
- Response
209
- }: {
210
- // eslint-disable-next-line @typescript-eslint/ban-types
211
- Request: REST.RequestSchemed<RequestA, RequestE>
212
- // eslint-disable-next-line @typescript-eslint/ban-types
213
- Response: REST.ReqRes<ResponseA, ResponseE, any>
214
- }) {
215
- const a = fetchApi2S(Request, Response)(
216
- Request.method,
217
- new Path(Request.path)
218
- )
219
- const parse = mapResponseM(S.encode(Response))
220
- return (req: RequestA) => Effect.flatMap(a(req), parse)
221
- }
222
-
223
- export function makePathWithQuery(
224
- path: Path,
225
- pars: Record<
226
- string,
227
- | string
228
- | number
229
- | boolean
230
- | readonly string[]
231
- | readonly number[]
232
- | readonly boolean[]
233
- | null
234
- >
235
- ) {
236
- const forQs = Record.filter(pars, (_, k) => !path.params.includes(k))
237
- const q = forQs // { ...forQs, _: JSON.stringify(forQs) } // TODO: drop completely individual keys from query?, sticking to json only
238
- return (
239
- path.build(pars, { ignoreSearch: true, ignoreConstraints: true })
240
- + (Object.keys(q).length
241
- ? "?" + qs.stringify(q)
242
- : "")
243
- )
244
- }
245
-
246
- export function makePathWithBody(
247
- path: Path,
248
- pars: Record<
249
- string,
250
- | string
251
- | number
252
- | boolean
253
- | readonly string[]
254
- | readonly number[]
255
- | readonly boolean[]
256
- | null
257
- >
258
- ) {
259
- return path.build(pars, { ignoreSearch: true, ignoreConstraints: true })
260
- }
261
-
262
- export function mapResponse<T, A>(map: (t: T) => A) {
263
- return (r: FetchResponse<T>): FetchResponse<A> => {
264
- return { ...r, body: map(r.body) }
265
- }
266
- }
267
-
268
- export function mapResponseM<T, R, E, A>(map: (t: T) => Effect<A, E, R>) {
269
- return (r: FetchResponse<T>): Effect<FetchResponse<A>, E, R> => {
270
- return Effect
271
- .all({
272
- body: map(r.body),
273
- headers: Effect.sync(() => r.headers),
274
- status: Effect.sync(() => r.status)
275
- })
276
- .pipe(Effect.withSpan("client.decode", { captureStackTrace: false }))
277
- }
278
- }
279
- export type FetchResponse<T> = { body: T; headers: Headers; status: number }
280
-
281
- export const EmptyResponse = Object.freeze({ body: null, headers: {}, status: 404 })
282
- export const EmptyResponseM = Effect.sync(() => EmptyResponse)
283
- const EmptyResponseMThunk_ = constant(EmptyResponseM)
284
- export function EmptyResponseMThunk<T>(): Effect<
285
- Readonly<{
286
- body: null | T
287
- // eslint-disable-next-line @typescript-eslint/ban-types
288
- headers: {}
289
- status: 404
290
- }>,
291
- never,
292
- unknown
293
- > {
294
- return EmptyResponseMThunk_()
295
- }
296
-
297
- export function getBody<R, E, A>(eff: Effect<FetchResponse<A | null>, E, R>) {
298
- return Effect.flatMap(eff, (r) => r.body === null ? Effect.die("Not found") : Effect.sync(() => r.body))
299
- }