diff --git a/README.md b/README.md
index b770542..3fc3fff 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ pnpm add @prover-coder-ai/openapi-effect
## Usage (Promise API)
-This package re-exports `openapi-fetch`, so most code can be migrated by changing only the import.
+This package implements an `openapi-fetch` compatible API, so most code can be migrated by changing only the import.
```ts
import createClient from "@prover-coder-ai/openapi-effect"
diff --git a/packages/app/eslint.config.mts b/packages/app/eslint.config.mts
index 301f43f..bf8875e 100644
--- a/packages/app/eslint.config.mts
+++ b/packages/app/eslint.config.mts
@@ -209,6 +209,26 @@ export default defineConfig(
message:
"Запрещены Promise.* — используй комбинаторы Effect (all, forEach, etc.).",
},
+ {
+ selector: "CallExpression[callee.object.object.name='globalThis'][callee.object.property.name='Promise']",
+ message:
+ "Запрещены globalThis.Promise.* — используй комбинаторы Effect.",
+ },
+ {
+ selector: "NewExpression[callee.object.name='globalThis'][callee.property.name='Promise']",
+ message:
+ "Запрещён globalThis.Promise — используй Effect.async / Effect.tryPromise.",
+ },
+ {
+ selector: "TSTypeReference[typeName.name='Promise'], TSTypeReference[typeName.name='PromiseLike']",
+ message:
+ "Запрещены Promise/PromiseLike в типах — используй Effect.Effect.",
+ },
+ {
+ selector: "TSTypeReference[typeName.type='TSQualifiedName'][typeName.left.name='globalThis'][typeName.right.name='Promise']",
+ message:
+ "Запрещён globalThis.Promise в типах — используй Effect.Effect.",
+ },
{
selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments",
message: "Do not use spread arguments in Array.push",
@@ -232,6 +252,18 @@ export default defineConfig(
"Запрещён Promise — используй Effect.Effect.",
suggest: ["Effect.Effect"],
},
+ PromiseLike: {
+ message:
+ "Запрещён PromiseLike — используй Effect.Effect.",
+ },
+ "PromiseLike<*>": {
+ message:
+ "Запрещён PromiseLike — используй Effect.Effect.",
+ },
+ "globalThis.Promise": {
+ message:
+ "Запрещён globalThis.Promise — используй Effect.Effect.",
+ },
},
},
],
@@ -312,6 +344,30 @@ export default defineConfig(
selector: 'CallExpression[callee.name="require"]',
message: "Avoid using require(). Use ES6 imports instead.",
},
+ {
+ selector: "NewExpression[callee.name='Promise']",
+ message: "Запрещён new Promise — используй Effect.async / Effect.tryPromise.",
+ },
+ {
+ selector: "CallExpression[callee.object.name='Promise']",
+ message: "Запрещены Promise.* — используй комбинаторы Effect.",
+ },
+ {
+ selector: "CallExpression[callee.object.object.name='globalThis'][callee.object.property.name='Promise']",
+ message: "Запрещены globalThis.Promise.* — используй комбинаторы Effect.",
+ },
+ {
+ selector: "NewExpression[callee.object.name='globalThis'][callee.property.name='Promise']",
+ message: "Запрещён globalThis.Promise — используй Effect.async / Effect.tryPromise.",
+ },
+ {
+ selector: "TSTypeReference[typeName.name='Promise'], TSTypeReference[typeName.name='PromiseLike']",
+ message: "Запрещены Promise/PromiseLike в типах — используй Effect.Effect.",
+ },
+ {
+ selector: "TSTypeReference[typeName.type='TSQualifiedName'][typeName.left.name='globalThis'][typeName.right.name='Promise']",
+ message: "Запрещён globalThis.Promise в типах — используй Effect.Effect.",
+ },
],
'@typescript-eslint/no-restricted-types': 'off',
// Axiom type casting functions intentionally use single-use type parameters
@@ -336,6 +392,30 @@ export default defineConfig(
selector: 'CallExpression[callee.name="require"]',
message: "Avoid using require(). Use ES6 imports instead.",
},
+ {
+ selector: "NewExpression[callee.name='Promise']",
+ message: "Запрещён new Promise — используй Effect.async / Effect.tryPromise.",
+ },
+ {
+ selector: "CallExpression[callee.object.name='Promise']",
+ message: "Запрещены Promise.* — используй комбинаторы Effect.",
+ },
+ {
+ selector: "CallExpression[callee.object.object.name='globalThis'][callee.object.property.name='Promise']",
+ message: "Запрещены globalThis.Promise.* — используй комбинаторы Effect.",
+ },
+ {
+ selector: "NewExpression[callee.object.name='globalThis'][callee.property.name='Promise']",
+ message: "Запрещён globalThis.Promise — используй Effect.async / Effect.tryPromise.",
+ },
+ {
+ selector: "TSTypeReference[typeName.name='Promise'], TSTypeReference[typeName.name='PromiseLike']",
+ message: "Запрещены Promise/PromiseLike в типах — используй Effect.Effect.",
+ },
+ {
+ selector: "TSTypeReference[typeName.type='TSQualifiedName'][typeName.left.name='globalThis'][typeName.right.name='Promise']",
+ message: "Запрещён globalThis.Promise в типах — используй Effect.Effect.",
+ },
],
'@typescript-eslint/no-restricted-types': 'off',
},
@@ -358,6 +438,6 @@ export default defineConfig(
extends: [tseslint.configs.disableTypeChecked],
},
- // 6) Глобальные игноры
+ // 7) Глобальные игноры
{ ignores: ['dist/**', 'build/**', 'coverage/**', '**/dist/**'] },
);
diff --git a/packages/app/eslint.effect-ts-check.config.mjs b/packages/app/eslint.effect-ts-check.config.mjs
index a08b380..373377c 100644
--- a/packages/app/eslint.effect-ts-check.config.mjs
+++ b/packages/app/eslint.effect-ts-check.config.mjs
@@ -79,6 +79,22 @@ const restrictedSyntaxBase = [
selector: "CallExpression[callee.object.name='Promise']",
message: "Avoid Promise.*. Use Effect combinators."
},
+ {
+ selector: "CallExpression[callee.object.object.name='globalThis'][callee.object.property.name='Promise']",
+ message: "Avoid globalThis.Promise.*. Use Effect combinators."
+ },
+ {
+ selector: "NewExpression[callee.object.name='globalThis'][callee.property.name='Promise']",
+ message: "Avoid globalThis.Promise. Use Effect.async / Effect.tryPromise."
+ },
+ {
+ selector: "TSTypeReference[typeName.name='Promise'], TSTypeReference[typeName.name='PromiseLike']",
+ message: "Avoid Promise/PromiseLike types. Use Effect.Effect."
+ },
+ {
+ selector: "TSTypeReference[typeName.type='TSQualifiedName'][typeName.left.name='globalThis'][typeName.right.name='Promise']",
+ message: "Avoid globalThis.Promise type. Use Effect.Effect."
+ },
{
selector: "CallExpression[callee.name='require']",
message: "Avoid require(). Use ES module imports."
@@ -178,6 +194,15 @@ export default tseslint.config(
},
"Promise<*>": {
message: "Avoid Promise. Use Effect.Effect."
+ },
+ PromiseLike: {
+ message: "Avoid PromiseLike. Use Effect.Effect."
+ },
+ "PromiseLike<*>": {
+ message: "Avoid PromiseLike. Use Effect.Effect."
+ },
+ "globalThis.Promise": {
+ message: "Avoid globalThis.Promise. Use Effect.Effect."
}
}
}],
diff --git a/packages/app/package.json b/packages/app/package.json
index d00645a..b8814d7 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -63,7 +63,6 @@
"@effect/typeclass": "^0.38.0",
"@effect/workflow": "^0.16.0",
"effect": "^3.19.16",
- "openapi-fetch": "^0.15.2",
"openapi-typescript-helpers": "^0.1.0",
"ts-morph": "^27.0.2"
},
diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts
index cec08a4..1879070 100644
--- a/packages/app/src/index.ts
+++ b/packages/app/src/index.ts
@@ -1,23 +1,16 @@
-// CHANGE: Make openapi-effect a drop-in replacement for openapi-fetch (Promise API), with an opt-in Effect API.
-// WHY: Consumer projects must be able to swap openapi-fetch -> openapi-effect with near-zero code changes.
-// QUOTE(ТЗ): "openapi-effect должен почти 1 в 1 заменяться с openapi-fetch" / "Просто добавлять effect поведение"
+// CHANGE: Expose Effect-only public API
+// WHY: Enforce Effect-first paradigm and remove Promise-based client surface
// SOURCE: n/a
// PURITY: SHELL (re-exports)
// COMPLEXITY: O(1)
-// Promise-based client (openapi-fetch compatible)
-export { default } from "openapi-fetch"
-export { default as createClient } from "openapi-fetch"
-export * from "openapi-fetch"
-
-// Effect-based client (opt-in)
export * as FetchHttpClient from "@effect/platform/FetchHttpClient"
-// Strict Effect client (advanced)
export type * from "./core/api-client/index.js"
export { assertNever } from "./core/api-client/index.js"
export type {
+ ClientOptions,
DispatchersFor,
StrictApiClient,
StrictApiClientWithDispatchers
@@ -26,17 +19,29 @@ export type {
export type { Decoder, Dispatcher, RawResponse, StrictClient, StrictRequestInit } from "./shell/api-client/index.js"
export {
- createClient as createClientStrict,
+ createClient,
createClientEffect,
createDispatcher,
+ createFinalURL,
+ createPathBasedClient,
+ createQuerySerializer,
createStrictClient,
createUniversalDispatcher,
+ defaultBodySerializer,
+ defaultPathSerializer,
executeRequest,
+ mergeHeaders,
parseJSON,
registerDefaultDispatchers,
+ removeTrailingSlash,
+ serializeArrayParam,
+ serializeObjectParam,
+ serializePrimitiveParam,
unexpectedContentType,
unexpectedStatus
} from "./shell/api-client/index.js"
+export { createClient as default } from "./shell/api-client/index.js"
+
// Generated dispatchers (auto-generated from OpenAPI schema)
export * from "./generated/index.js"
diff --git a/packages/app/src/shell/api-client/create-client-middleware.ts b/packages/app/src/shell/api-client/create-client-middleware.ts
new file mode 100644
index 0000000..6694c05
--- /dev/null
+++ b/packages/app/src/shell/api-client/create-client-middleware.ts
@@ -0,0 +1,179 @@
+import { Effect } from "effect"
+
+import { toError } from "./create-client-response.js"
+import type { AsyncValue, MergedOptions, Middleware, MiddlewareRequestParams, Thenable } from "./create-client-types.js"
+
+const isThenable = (value: unknown): value is Thenable => (
+ typeof value === "object"
+ && value !== null
+ && "then" in value
+ && typeof Reflect.get(value, "then") === "function"
+)
+
+export const toPromiseEffect = (value: AsyncValue): Effect.Effect => (
+ isThenable(value)
+ ? Effect.async((resume) => {
+ value.then(
+ (result) => {
+ resume(Effect.succeed(result))
+ },
+ (error) => {
+ resume(Effect.fail(toError(error)))
+ }
+ )
+ })
+ : Effect.succeed(value)
+)
+
+export type MiddlewareContext = {
+ schemaPath: string
+ params: MiddlewareRequestParams
+ options: MergedOptions
+ id: string
+ middleware: Array
+}
+
+const reverseMiddleware = (middleware: Array): Array => {
+ const output: Array = []
+
+ for (let index = middleware.length - 1; index >= 0; index -= 1) {
+ const item = middleware[index]
+ if (item !== undefined) {
+ output.push(item)
+ }
+ }
+
+ return output
+}
+
+type RequestMiddlewareResult = {
+ request: Request
+ response?: Response
+}
+
+const createMiddlewareParams = (
+ request: Request,
+ context: MiddlewareContext
+): {
+ request: Request
+ schemaPath: string
+ params: MiddlewareRequestParams
+ options: MergedOptions
+ id: string
+} => ({
+ request,
+ schemaPath: context.schemaPath,
+ params: context.params,
+ options: context.options,
+ id: context.id
+})
+
+export const applyRequestMiddleware = (
+ request: Request,
+ context: MiddlewareContext
+): Effect.Effect =>
+ Effect.gen(function*() {
+ let nextRequest = request
+
+ for (const item of context.middleware) {
+ if (typeof item.onRequest !== "function") {
+ continue
+ }
+
+ const result = yield* toPromiseEffect(item.onRequest(createMiddlewareParams(nextRequest, context)))
+
+ if (result === undefined) {
+ continue
+ }
+
+ if (result instanceof Request) {
+ nextRequest = result
+ continue
+ }
+
+ if (result instanceof Response) {
+ return { request: nextRequest, response: result }
+ }
+
+ return yield* Effect.fail(
+ new Error("onRequest: must return new Request() or Response() when modifying the request")
+ )
+ }
+
+ return { request: nextRequest }
+ })
+
+export const applyResponseMiddleware = (
+ request: Request,
+ response: Response,
+ context: MiddlewareContext
+): Effect.Effect =>
+ Effect.gen(function*() {
+ let nextResponse = response
+
+ for (const item of reverseMiddleware(context.middleware)) {
+ if (typeof item.onResponse !== "function") {
+ continue
+ }
+
+ const result = yield* toPromiseEffect(item.onResponse({
+ ...createMiddlewareParams(request, context),
+ response: nextResponse
+ }))
+
+ if (result === undefined) {
+ continue
+ }
+
+ if (!(result instanceof Response)) {
+ return yield* Effect.fail(
+ new Error("onResponse: must return new Response() when modifying the response")
+ )
+ }
+
+ nextResponse = result
+ }
+
+ return nextResponse
+ })
+
+const normalizeErrorResult = (
+ result: Response | Error | undefined
+): Effect.Effect => {
+ if (result === undefined || result instanceof Response || result instanceof Error) {
+ return Effect.succeed(result)
+ }
+
+ return Effect.fail(new Error("onError: must return new Response() or instance of Error"))
+}
+
+export const applyErrorMiddleware = (
+ request: Request,
+ fetchError: Error,
+ context: MiddlewareContext
+): Effect.Effect =>
+ Effect.gen(function*() {
+ let nextError: Error = fetchError
+
+ for (const item of reverseMiddleware(context.middleware)) {
+ if (typeof item.onError !== "function") {
+ continue
+ }
+
+ const rawResult = yield* toPromiseEffect(item.onError({
+ ...createMiddlewareParams(request, context),
+ error: nextError
+ }))
+
+ const result = yield* normalizeErrorResult(rawResult)
+ if (result instanceof Response) {
+ return result
+ }
+
+ if (result instanceof Error) {
+ nextError = result
+ }
+ }
+
+ return yield* Effect.fail(nextError)
+ })
diff --git a/packages/app/src/shell/api-client/create-client-response.ts b/packages/app/src/shell/api-client/create-client-response.ts
new file mode 100644
index 0000000..58601ae
--- /dev/null
+++ b/packages/app/src/shell/api-client/create-client-response.ts
@@ -0,0 +1,115 @@
+import { Effect } from "effect"
+
+import { asJson } from "../../core/axioms.js"
+import type { ParseAs } from "./create-client-types.js"
+
+type RuntimeFetchResponse = {
+ data?: unknown
+ error?: unknown
+ response: Response
+}
+
+export const toError = (error: unknown): Error => (
+ error instanceof Error ? error : new Error(String(error))
+)
+
+const parseJsonText = (rawText: string): Effect.Effect => (
+ rawText.length === 0
+ ? Effect.void
+ : Effect.try({
+ try: () => asJson(JSON.parse(rawText)),
+ catch: toError
+ })
+)
+
+const readResponseText = (response: Response): Effect.Effect => (
+ Effect.tryPromise({
+ try: () => response.text(),
+ catch: toError
+ })
+)
+
+const parseSuccessData = (
+ response: Response,
+ parseAs: ParseAs,
+ contentLength: string | null
+): Effect.Effect => {
+ if (parseAs === "stream") {
+ return Effect.succeed(response.body)
+ }
+
+ if (parseAs === "text") {
+ return Effect.tryPromise({ try: () => response.text(), catch: toError })
+ }
+
+ if (parseAs === "blob") {
+ return Effect.tryPromise({ try: () => response.blob(), catch: toError })
+ }
+
+ if (parseAs === "arrayBuffer") {
+ return Effect.tryPromise({ try: () => response.arrayBuffer(), catch: toError })
+ }
+
+ if (contentLength === null) {
+ return readResponseText(response).pipe(
+ Effect.flatMap((rawText) => parseJsonText(rawText))
+ )
+ }
+
+ return Effect.tryPromise({ try: () => response.json(), catch: toError })
+}
+
+const parseErrorData = (response: Response): Effect.Effect => (
+ readResponseText(response).pipe(
+ Effect.flatMap((rawText) =>
+ Effect.match(
+ Effect.try({
+ try: () => asJson(JSON.parse(rawText)),
+ catch: toError
+ }),
+ {
+ onFailure: () => rawText,
+ onSuccess: (parsed) => parsed
+ }
+ )
+ )
+ )
+)
+
+const hasChunkedTransferEncoding = (response: Response): boolean => (
+ response.headers.get("Transfer-Encoding")?.includes("chunked") === true
+)
+
+const isEmptyResponse = (
+ request: Request,
+ response: Response,
+ contentLength: string | null
+): boolean => (
+ response.status === 204
+ || request.method === "HEAD"
+ || (contentLength === "0" && !hasChunkedTransferEncoding(response))
+)
+
+export const createResponseEnvelope = (
+ request: Request,
+ response: Response,
+ parseAs: ParseAs
+): Effect.Effect => {
+ const contentLength = response.headers.get("Content-Length")
+
+ if (isEmptyResponse(request, response, contentLength)) {
+ return response.ok
+ ? Effect.succeed({ data: undefined, response })
+ : Effect.succeed({ error: undefined, response })
+ }
+
+ if (response.ok) {
+ return parseSuccessData(response, parseAs, contentLength).pipe(
+ Effect.map((data) => ({ data, response }))
+ )
+ }
+
+ return parseErrorData(response).pipe(
+ Effect.map((error) => ({ error, response }))
+ )
+}
diff --git a/packages/app/src/shell/api-client/create-client-runtime-helpers.ts b/packages/app/src/shell/api-client/create-client-runtime-helpers.ts
new file mode 100644
index 0000000..0bb36c4
--- /dev/null
+++ b/packages/app/src/shell/api-client/create-client-runtime-helpers.ts
@@ -0,0 +1,142 @@
+import { Effect } from "effect"
+
+import { asStrictApiClient } from "../../core/axioms.js"
+import { toError } from "./create-client-response.js"
+import type { FetchWithRequestInitExt, HeaderRecord } from "./create-client-runtime-types.js"
+import type {
+ BodySerializer,
+ ClientOptions,
+ MergedOptions,
+ MiddlewareRequestParams,
+ ParseAs,
+ PathSerializer,
+ QuerySerializer,
+ QuerySerializerOptions
+} from "./create-client-types.js"
+import { createQuerySerializer } from "./openapi-compat-utils.js"
+
+export const supportsRequestInitExt = (): boolean => (
+ typeof process === "object"
+ && Number.parseInt(process.versions.node.slice(0, 2), 10) >= 18
+ && typeof process.versions["undici"] === "string"
+)
+
+export const randomID = (): string => (
+ globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, 9)
+)
+
+const isQuerySerializerOptions = (
+ value: QuerySerializer | QuerySerializerOptions | undefined
+): value is QuerySerializerOptions => (
+ value !== undefined && typeof value === "object"
+)
+
+export const resolveQuerySerializer = (
+ globalQuerySerializer: ClientOptions["querySerializer"],
+ requestQuerySerializer: QuerySerializer | QuerySerializerOptions | undefined
+): QuerySerializer => {
+ let serializer = typeof globalQuerySerializer === "function"
+ ? globalQuerySerializer
+ : createQuerySerializer(globalQuerySerializer)
+
+ if (requestQuerySerializer) {
+ serializer = typeof requestQuerySerializer === "function"
+ ? requestQuerySerializer
+ : createQuerySerializer({
+ ...(isQuerySerializerOptions(globalQuerySerializer) ? globalQuerySerializer : {}),
+ ...requestQuerySerializer
+ })
+ }
+
+ return serializer
+}
+
+const isHeaderPrimitive = (value: unknown): value is string | number | boolean => (
+ typeof value === "string" || typeof value === "number" || typeof value === "boolean"
+)
+
+export const toHeaderOverrides = (headers: MiddlewareRequestParams["header"]): HeaderRecord => {
+ if (headers === undefined) {
+ return {}
+ }
+
+ const normalized: HeaderRecord = {}
+ for (const [key, rawValue] of Object.entries(headers)) {
+ if (rawValue === undefined || rawValue === null || isHeaderPrimitive(rawValue)) {
+ normalized[key] = rawValue
+ continue
+ }
+
+ if (Array.isArray(rawValue)) {
+ normalized[key] = rawValue.filter((item) => isHeaderPrimitive(item))
+ }
+ }
+
+ return normalized
+}
+
+const isBodyInit = (value: BodyInit | object): value is BodyInit => (
+ typeof value === "string"
+ || value instanceof Blob
+ || value instanceof URLSearchParams
+ || value instanceof ArrayBuffer
+ || value instanceof FormData
+ || value instanceof ReadableStream
+)
+
+export type SerializedBody =
+ | { hasBody: false }
+ | { hasBody: true; value: BodyInit }
+
+export const serializeBody = (
+ body: BodyInit | object | undefined,
+ serializer: BodySerializer,
+ headers: Headers
+): SerializedBody => {
+ if (body === undefined) {
+ return { hasBody: false }
+ }
+
+ if (isBodyInit(body)) {
+ return { hasBody: true, value: body }
+ }
+
+ return { hasBody: true, value: serializer(body, headers) }
+}
+
+export const setCustomRequestFields = (request: Request, init: Record): void => {
+ for (const key in init) {
+ if (!(key in request)) {
+ Reflect.set(request, key, init[key])
+ }
+ }
+}
+
+export const invokeFetch = (
+ fetch: NonNullable,
+ request: Request,
+ requestInitExt?: Record
+): Effect.Effect => {
+ const fetchWithExt = asStrictApiClient(fetch)
+ return Effect.tryPromise({
+ try: () => fetchWithExt(request, requestInitExt),
+ catch: toError
+ })
+}
+
+export const createMergedOptions = (options: {
+ baseUrl: string
+ parseAs: ParseAs
+ querySerializer: QuerySerializer
+ bodySerializer: BodySerializer
+ pathSerializer: PathSerializer
+ fetch: NonNullable
+}): MergedOptions =>
+ Object.freeze({
+ baseUrl: options.baseUrl,
+ parseAs: options.parseAs,
+ querySerializer: options.querySerializer,
+ bodySerializer: options.bodySerializer,
+ pathSerializer: options.pathSerializer,
+ fetch: asStrictApiClient(options.fetch)
+ })
diff --git a/packages/app/src/shell/api-client/create-client-runtime-types.ts b/packages/app/src/shell/api-client/create-client-runtime-types.ts
new file mode 100644
index 0000000..51304e6
--- /dev/null
+++ b/packages/app/src/shell/api-client/create-client-runtime-types.ts
@@ -0,0 +1,97 @@
+import type { Effect } from "effect"
+
+import type { MiddlewareContext } from "./create-client-middleware.js"
+import type {
+ BodySerializer,
+ ClientOptions,
+ HeadersOptions,
+ Middleware,
+ MiddlewareRequestParams,
+ ParseAs,
+ PathSerializer,
+ QuerySerializer,
+ QuerySerializerOptions
+} from "./create-client-types.js"
+
+export type RuntimeFetchResponse = {
+ data?: unknown
+ error?: unknown
+ response: Response
+}
+
+export type RuntimeFetchOptions = Omit & {
+ baseUrl?: string
+ fetch?: NonNullable
+ Request?: ClientOptions["Request"]
+ headers?: HeadersOptions
+ params?: MiddlewareRequestParams
+ parseAs?: ParseAs
+ querySerializer?: QuerySerializer | QuerySerializerOptions
+ pathSerializer?: PathSerializer
+ bodySerializer?: BodySerializer
+ body?: BodyInit | object
+ middleware?: Array
+ method?: string
+ [key: string]: unknown
+}
+
+export type RuntimeClient = {
+ request: (method: string, url: string, init?: RuntimeFetchOptions) => Effect.Effect
+ GET: (url: string, init?: RuntimeFetchOptions) => Effect.Effect
+ PUT: (url: string, init?: RuntimeFetchOptions) => Effect.Effect
+ POST: (url: string, init?: RuntimeFetchOptions) => Effect.Effect
+ DELETE: (url: string, init?: RuntimeFetchOptions) => Effect.Effect
+ OPTIONS: (url: string, init?: RuntimeFetchOptions) => Effect.Effect
+ HEAD: (url: string, init?: RuntimeFetchOptions) => Effect.Effect
+ PATCH: (url: string, init?: RuntimeFetchOptions) => Effect.Effect
+ TRACE: (url: string, init?: RuntimeFetchOptions) => Effect.Effect
+ use: (...middleware: Array) => void
+ eject: (...middleware: Array) => void
+}
+
+export type HeaderValue =
+ | string
+ | number
+ | boolean
+ | Array
+ | null
+ | undefined
+
+export type HeaderRecord = Record
+
+export type BaseRuntimeConfig = {
+ Request: typeof Request
+ baseUrl: string
+ bodySerializer: BodySerializer | undefined
+ fetch: NonNullable
+ pathSerializer: PathSerializer | undefined
+ headers: HeadersOptions | undefined
+ querySerializer: QuerySerializer | QuerySerializerOptions | undefined
+ requestInitExt: Record | undefined
+ baseOptions: Omit<
+ ClientOptions,
+ | "Request"
+ | "baseUrl"
+ | "bodySerializer"
+ | "fetch"
+ | "headers"
+ | "querySerializer"
+ | "pathSerializer"
+ | "requestInitExt"
+ >
+ globalMiddlewares: Array
+}
+
+export type PreparedRequest = {
+ request: Request
+ fetch: NonNullable
+ parseAs: ParseAs
+ context: MiddlewareContext
+ middleware: Array
+ requestInitExt: Record | undefined
+}
+
+export type FetchWithRequestInitExt = (
+ input: Request,
+ requestInitExt?: Record
+) => ReturnType
diff --git a/packages/app/src/shell/api-client/create-client-runtime.ts b/packages/app/src/shell/api-client/create-client-runtime.ts
new file mode 100644
index 0000000..adef39f
--- /dev/null
+++ b/packages/app/src/shell/api-client/create-client-runtime.ts
@@ -0,0 +1,316 @@
+import { Effect } from "effect"
+
+import { applyErrorMiddleware, applyRequestMiddleware, applyResponseMiddleware } from "./create-client-middleware.js"
+import { createResponseEnvelope } from "./create-client-response.js"
+import {
+ createMergedOptions,
+ invokeFetch,
+ randomID,
+ resolveQuerySerializer,
+ serializeBody,
+ setCustomRequestFields,
+ supportsRequestInitExt,
+ toHeaderOverrides
+} from "./create-client-runtime-helpers.js"
+import type { SerializedBody } from "./create-client-runtime-helpers.js"
+import type {
+ BaseRuntimeConfig,
+ PreparedRequest,
+ RuntimeClient,
+ RuntimeFetchOptions,
+ RuntimeFetchResponse
+} from "./create-client-runtime-types.js"
+import type {
+ BodySerializer,
+ ClientOptions,
+ Middleware,
+ MiddlewareRequestParams,
+ ParseAs,
+ PathSerializer,
+ QuerySerializer
+} from "./create-client-types.js"
+import {
+ createFinalURL,
+ defaultBodySerializer,
+ defaultPathSerializer,
+ mergeHeaders,
+ removeTrailingSlash
+} from "./openapi-compat-utils.js"
+
+type ResolvedFetchConfig = {
+ Request: typeof Request
+ fetch: NonNullable
+ parseAs: ParseAs
+ params: MiddlewareRequestParams
+ body: BodyInit | object | undefined
+ bodySerializer: BodySerializer
+ headers: ClientOptions["headers"]
+ init: Record
+ finalBaseUrl: string
+ pathSerializer: PathSerializer
+ querySerializer: QuerySerializer
+ middleware: Array
+}
+
+const resolveBaseUrl = (baseUrl: string, localBaseUrl: string | undefined): string => (
+ localBaseUrl ? removeTrailingSlash(localBaseUrl) : baseUrl
+)
+
+const resolveBodySerializer = (
+ globalBodySerializer: BodySerializer | undefined,
+ requestBodySerializer: BodySerializer | undefined
+): BodySerializer => (
+ requestBodySerializer ?? globalBodySerializer ?? defaultBodySerializer
+)
+
+const resolvePathSerializer = (
+ globalPathSerializer: PathSerializer | undefined,
+ requestPathSerializer: PathSerializer | undefined
+): PathSerializer => (
+ requestPathSerializer ?? globalPathSerializer ?? defaultPathSerializer
+)
+
+const joinMiddleware = (
+ globalMiddlewares: Array,
+ requestMiddlewares: Array
+): Array => [...globalMiddlewares, ...requestMiddlewares]
+
+const resolveFetchConfig = (
+ config: BaseRuntimeConfig,
+ fetchOptions?: RuntimeFetchOptions
+): ResolvedFetchConfig => {
+ const {
+ Request = config.Request,
+ baseUrl: localBaseUrl,
+ body,
+ bodySerializer: requestBodySerializer,
+ fetch = config.fetch,
+ headers,
+ middleware: requestMiddlewares = [],
+ params = {},
+ parseAs = "json",
+ pathSerializer: requestPathSerializer,
+ querySerializer: requestQuerySerializer,
+ ...init
+ } = fetchOptions ?? {}
+
+ return {
+ Request,
+ fetch,
+ parseAs,
+ params,
+ body,
+ bodySerializer: resolveBodySerializer(config.bodySerializer, requestBodySerializer),
+ headers,
+ init,
+ finalBaseUrl: resolveBaseUrl(config.baseUrl, localBaseUrl),
+ pathSerializer: resolvePathSerializer(config.pathSerializer, requestPathSerializer),
+ querySerializer: resolveQuerySerializer(config.querySerializer, requestQuerySerializer),
+ middleware: joinMiddleware(config.globalMiddlewares, requestMiddlewares)
+ }
+}
+
+type ResolvedHeaders = {
+ serializedBody: SerializedBody
+ finalHeaders: Headers
+}
+
+const resolveHeaders = (
+ config: BaseRuntimeConfig,
+ resolved: ResolvedFetchConfig
+): ResolvedHeaders => {
+ const headerOverrides = toHeaderOverrides(resolved.params.header)
+ const serializedBody = serializeBody(
+ resolved.body,
+ resolved.bodySerializer,
+ mergeHeaders(config.headers, resolved.headers, headerOverrides)
+ )
+
+ const finalHeaders = mergeHeaders(
+ !serializedBody.hasBody || serializedBody.value instanceof FormData
+ ? {}
+ : { "Content-Type": "application/json" },
+ config.headers,
+ resolved.headers,
+ headerOverrides
+ )
+
+ return { serializedBody, finalHeaders }
+}
+
+const createRequest = (
+ config: BaseRuntimeConfig,
+ schemaPath: string,
+ resolved: ResolvedFetchConfig,
+ resolvedHeaders: ResolvedHeaders
+): Request => {
+ const requestInit: RequestInit = {
+ redirect: "follow",
+ ...config.baseOptions,
+ ...resolved.init,
+ ...(resolvedHeaders.serializedBody.hasBody
+ ? { body: resolvedHeaders.serializedBody.value }
+ : {}),
+ headers: resolvedHeaders.finalHeaders
+ }
+
+ const request = new resolved.Request(
+ createFinalURL(schemaPath, {
+ baseUrl: resolved.finalBaseUrl,
+ params: resolved.params,
+ querySerializer: resolved.querySerializer,
+ pathSerializer: resolved.pathSerializer
+ }),
+ requestInit
+ )
+
+ setCustomRequestFields(request, resolved.init)
+ return request
+}
+
+const createPreparedContext = (
+ schemaPath: string,
+ resolved: ResolvedFetchConfig
+): PreparedRequest["context"] => ({
+ schemaPath,
+ params: resolved.params,
+ id: randomID(),
+ options: createMergedOptions({
+ baseUrl: resolved.finalBaseUrl,
+ parseAs: resolved.parseAs,
+ querySerializer: resolved.querySerializer,
+ bodySerializer: resolved.bodySerializer,
+ pathSerializer: resolved.pathSerializer,
+ fetch: resolved.fetch
+ }),
+ middleware: resolved.middleware
+})
+
+const prepareRequest = (
+ config: BaseRuntimeConfig,
+ schemaPath: string,
+ fetchOptions?: RuntimeFetchOptions
+): PreparedRequest => {
+ const resolved = resolveFetchConfig(config, fetchOptions)
+ const requestHeaders = resolveHeaders(config, resolved)
+ const request = createRequest(config, schemaPath, resolved, requestHeaders)
+
+ return {
+ request,
+ fetch: resolved.fetch,
+ parseAs: resolved.parseAs,
+ middleware: resolved.middleware,
+ requestInitExt: config.requestInitExt,
+ context: createPreparedContext(schemaPath, resolved)
+ }
+}
+
+const executeFetch = (
+ prepared: PreparedRequest
+): Effect.Effect<{ request: Request; response: Response }, Error> => {
+ if (prepared.middleware.length === 0) {
+ return invokeFetch(prepared.fetch, prepared.request, prepared.requestInitExt).pipe(
+ Effect.map((response) => ({ request: prepared.request, response }))
+ )
+ }
+
+ return Effect.gen(function*() {
+ const requestPhase = yield* applyRequestMiddleware(prepared.request, prepared.context)
+ const request = requestPhase.request
+
+ const response = requestPhase.response ?? (
+ yield* invokeFetch(prepared.fetch, request, prepared.requestInitExt).pipe(
+ Effect.matchEffect({
+ onFailure: (fetchError) => applyErrorMiddleware(request, fetchError, prepared.context),
+ onSuccess: (response) => Effect.succeed(response)
+ })
+ )
+ )
+
+ const responseAfterMiddleware = yield* applyResponseMiddleware(request, response, prepared.context)
+ return { request, response: responseAfterMiddleware }
+ })
+}
+
+const createCoreFetch = (config: BaseRuntimeConfig) =>
+(
+ schemaPath: string,
+ fetchOptions?: RuntimeFetchOptions
+): Effect.Effect =>
+ Effect.gen(function*() {
+ const prepared = prepareRequest(config, schemaPath, fetchOptions)
+ const execution = yield* executeFetch(prepared)
+ return yield* createResponseEnvelope(execution.request, execution.response, prepared.parseAs)
+ })
+
+const hasMiddlewareHook = (value: Middleware): boolean => (
+ "onRequest" in value || "onResponse" in value || "onError" in value
+)
+
+const createBaseRuntimeConfig = (
+ clientOptions: ClientOptions | undefined,
+ globalMiddlewares: Array
+): BaseRuntimeConfig => {
+ const {
+ Request = globalThis.Request,
+ baseUrl: rawBaseUrl = "",
+ bodySerializer,
+ fetch = globalThis.fetch,
+ headers,
+ pathSerializer,
+ querySerializer,
+ requestInitExt: rawRequestInitExt,
+ ...baseOptions
+ } = { ...clientOptions }
+
+ return {
+ Request,
+ baseUrl: removeTrailingSlash(rawBaseUrl),
+ bodySerializer,
+ fetch,
+ headers,
+ pathSerializer,
+ querySerializer,
+ requestInitExt: supportsRequestInitExt() ? rawRequestInitExt : undefined,
+ baseOptions,
+ globalMiddlewares
+ }
+}
+
+const createClientMethods = (
+ coreFetch: ReturnType,
+ globalMiddlewares: Array
+): RuntimeClient => ({
+ request: (method, url, init) => coreFetch(url, { ...init, method: method.toUpperCase() }),
+ GET: (url, init) => coreFetch(url, { ...init, method: "GET" }),
+ PUT: (url, init) => coreFetch(url, { ...init, method: "PUT" }),
+ POST: (url, init) => coreFetch(url, { ...init, method: "POST" }),
+ DELETE: (url, init) => coreFetch(url, { ...init, method: "DELETE" }),
+ OPTIONS: (url, init) => coreFetch(url, { ...init, method: "OPTIONS" }),
+ HEAD: (url, init) => coreFetch(url, { ...init, method: "HEAD" }),
+ PATCH: (url, init) => coreFetch(url, { ...init, method: "PATCH" }),
+ TRACE: (url, init) => coreFetch(url, { ...init, method: "TRACE" }),
+ use: (...middleware) => {
+ for (const item of middleware) {
+ if (!hasMiddlewareHook(item)) {
+ throw new Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`")
+ }
+ globalMiddlewares.push(item)
+ }
+ },
+ eject: (...middleware) => {
+ for (const item of middleware) {
+ const index = globalMiddlewares.indexOf(item)
+ if (index !== -1) {
+ globalMiddlewares.splice(index, 1)
+ }
+ }
+ }
+})
+
+export const createRuntimeClient = (clientOptions?: ClientOptions): RuntimeClient => {
+ const globalMiddlewares: Array = []
+ const config = createBaseRuntimeConfig(clientOptions, globalMiddlewares)
+ const coreFetch = createCoreFetch(config)
+ return createClientMethods(coreFetch, globalMiddlewares)
+}
diff --git a/packages/app/src/shell/api-client/create-client-types.ts b/packages/app/src/shell/api-client/create-client-types.ts
index 34970e7..12f6fcf 100644
--- a/packages/app/src/shell/api-client/create-client-types.ts
+++ b/packages/app/src/shell/api-client/create-client-types.ts
@@ -1,310 +1,302 @@
-// CHANGE: Extract public createClient types into dedicated module
-// WHY: Keep create-client.ts under lint max-lines without weakening type-level invariants
-// QUOTE(ТЗ): "Только прогони по всему проекту линтеры"
-// REF: user-msg-2
-// SOURCE: n/a
-// PURITY: CORE
-// EFFECT: none
-// INVARIANT: All type-level correlations remain unchanged
-// COMPLEXITY: O(1) compile-time / O(0) runtime
-
-import type * as HttpClient from "@effect/platform/HttpClient"
import type { Effect } from "effect"
-import type { ClientOptions as OpenapiFetchClientOptions } from "openapi-fetch"
-import type { HttpMethod } from "openapi-typescript-helpers"
-
import type {
- ApiFailure,
- ApiSuccess,
- HttpError,
- OperationFor,
- PathsForMethod,
- RequestOptionsFor,
- ResponsesFor
-} from "../../core/api-client/strict-types.js"
-import type { Dispatcher } from "../../core/axioms.js"
-
-/**
- * Client configuration options
- *
- * @pure - immutable configuration
- */
-export type ClientOptions = OpenapiFetchClientOptions
-
-// CHANGE: Add dispatcher map type for auto-dispatching clients
-// WHY: Enable creating clients that infer dispatcher from path+method without per-call parameter
-// QUOTE(ТЗ): "ApiClient и так знает текущие типы. Зачем передавать что либо в GET"
-// REF: user-msg-1
-// SOURCE: n/a
-// FORMAT THEOREM: ∀ path, method: dispatchers[path][method] = Dispatcher>>
-// PURITY: CORE
-// EFFECT: none
-// INVARIANT: dispatcher map is total for all operations in Paths
-// COMPLEXITY: O(1) compile-time / O(0) runtime
-export type DispatchersForMethod<
- Paths extends object,
- Method extends HttpMethod
-> = {
- readonly [Path in PathsForMethod]: {
- readonly [K in Method]: Dispatcher>>
+ ErrorResponse,
+ FilterKeys,
+ HttpMethod,
+ IsOperationRequestBodyOptional,
+ MediaType,
+ OperationRequestBodyContent,
+ PathsWithMethod,
+ Readable,
+ RequiredKeysOf,
+ ResponseObjectMap,
+ SuccessResponse,
+ Writable
+} from "openapi-typescript-helpers"
+
+export interface ClientOptions extends Omit {
+ baseUrl?: string
+ fetch?: (input: Request) => ReturnType
+ Request?: typeof Request
+ querySerializer?: QuerySerializer | QuerySerializerOptions
+ bodySerializer?: BodySerializer
+ pathSerializer?: PathSerializer
+ headers?: HeadersOptions
+ requestInitExt?: Record
+}
+
+export type HeadersOptions =
+ | Required["headers"]
+ | Record<
+ string,
+ string | number | boolean | Array | null | undefined
+ >
+
+export type QuerySerializer = (
+ query: T extends { parameters: infer Parameters } ? Parameters extends { query?: infer Query } ? NonNullable
+ : Record
+ : Record
+) => string
+
+export type QuerySerializerOptions = {
+ array?: {
+ style: "form" | "spaceDelimited" | "pipeDelimited"
+ explode: boolean
+ }
+ object?: {
+ style: "form" | "deepObject"
+ explode: boolean
+ }
+ allowReserved?: boolean
+}
+
+export type BodySerializer = (
+ body: Writable> | BodyInit | object,
+ headers?: Headers | HeadersOptions
+) => BodyInit
+
+export type PathSerializer = (
+ pathname: string,
+ pathParams: Record
+) => string
+
+type BodyType = {
+ json: T
+ text: Awaited>
+ blob: Awaited>
+ arrayBuffer: Awaited>
+ stream: Response["body"]
+}
+
+export type ParseAs = keyof BodyType
+
+export type ParseAsResponse = Options extends { parseAs: ParseAs } ? BodyType[Options["parseAs"]]
+ : T
+
+export interface DefaultParamsOption {
+ params?: {
+ query?: Record
+ }
+}
+
+export type ParamsOption = T extends { parameters: infer Parameters }
+ ? RequiredKeysOf extends never ? { params?: Parameters }
+ : { params: Parameters }
+ : DefaultParamsOption
+
+export type RequestBodyOption = Writable> extends never ? { body?: never }
+ : IsOperationRequestBodyOptional extends true ? { body?: Writable> }
+ : { body: Writable> }
+
+export type FetchOptions = RequestOptions & Omit
+
+export type FetchResponse<
+ T extends Record,
+ Options,
+ Media extends MediaType
+> =
+ | {
+ data: ParseAsResponse, Media>>, Options>
+ error?: never
+ response: Response
+ }
+ | {
+ data?: never
+ error: Readable, Media>>
+ response: Response
+ }
+
+export type RequestOptions =
+ & ParamsOption
+ & RequestBodyOption
+ & {
+ baseUrl?: string
+ querySerializer?: QuerySerializer | QuerySerializerOptions
+ bodySerializer?: BodySerializer
+ pathSerializer?: PathSerializer
+ parseAs?: ParseAs
+ fetch?: ClientOptions["fetch"]
+ headers?: HeadersOptions
+ middleware?: Array
}
+
+export type MergedOptions = {
+ baseUrl: string
+ parseAs: ParseAs
+ querySerializer: QuerySerializer
+ bodySerializer: BodySerializer
+ pathSerializer: PathSerializer
+ fetch: typeof globalThis.fetch
+}
+
+export interface MiddlewareRequestParams {
+ query?: Record
+ header?: Record
+ path?: Record
+ cookie?: Record
+}
+
+export interface MiddlewareCallbackParams {
+ request: Request
+ readonly schemaPath: string
+ readonly params: MiddlewareRequestParams
+ readonly id: string
+ readonly options: MergedOptions
+}
+
+export type Thenable = {
+ then: (
+ onFulfilled: (value: T) => unknown,
+ onRejected?: (reason: unknown) => unknown
+ ) => unknown
}
-export type DispatchersFor =
- & DispatchersForMethod
- & DispatchersForMethod
- & DispatchersForMethod
- & DispatchersForMethod
- & DispatchersForMethod
- & DispatchersForMethod
- & DispatchersForMethod
+export type AsyncValue = T | Thenable
+
+export type MiddlewareOnRequest = (
+ options: MiddlewareCallbackParams
+) => AsyncValue
+
+export type MiddlewareOnResponse = (
+ options: MiddlewareCallbackParams & { response: Response }
+) => AsyncValue
+
+export type MiddlewareOnError = (
+ options: MiddlewareCallbackParams & { error: unknown }
+) => AsyncValue
+
+export type Middleware =
+ | {
+ onRequest: MiddlewareOnRequest
+ onResponse?: MiddlewareOnResponse
+ onError?: MiddlewareOnError
+ }
+ | {
+ onRequest?: MiddlewareOnRequest
+ onResponse: MiddlewareOnResponse
+ onError?: MiddlewareOnError
+ }
+ | {
+ onRequest?: MiddlewareOnRequest
+ onResponse?: MiddlewareOnResponse
+ onError: MiddlewareOnError
+ }
+
+export type MaybeOptionalInit = RequiredKeysOf<
+ FetchOptions>
+> extends never ? FetchOptions> | undefined
+ : FetchOptions>
+
+type InitParam = RequiredKeysOf extends never ? [(Init & { [key: string]: unknown })?]
+ : [Init & { [key: string]: unknown }]
-type ResponsesForOperation<
+type OperationFor<
Paths extends object,
Path extends keyof Paths,
Method extends HttpMethod
-> = ResponsesFor>
+> = Paths[Path] extends Record ? Operation & Record
+ : never
-type RequestEffect<
+type MethodResult<
Paths extends object,
- Path extends keyof Paths,
- Method extends HttpMethod
+ Path extends PathsWithMethod,
+ Method extends HttpMethod,
+ Init,
+ Media extends MediaType
> = Effect.Effect<
- ApiSuccess>,
- ApiFailure>,
- HttpClient.HttpClient
+ FetchResponse, Init, Media>,
+ Error
>
-type RequestEffectWithHttpErrorsInSuccess<
+export type ClientMethod<
Paths extends object,
- Path extends keyof Paths,
- Method extends HttpMethod
+ Method extends HttpMethod,
+ Media extends MediaType
+> = <
+ Path extends PathsWithMethod,
+ Init extends MaybeOptionalInit>
+>(
+ url: Path,
+ ...init: InitParam
+) => MethodResult
+
+export type ClientRequestMethod<
+ Paths extends object,
+ Media extends MediaType
+> = <
+ Method extends HttpMethod,
+ Path extends PathsWithMethod,
+ Init extends MaybeOptionalInit>
+>(
+ method: Method,
+ url: Path,
+ ...init: InitParam
+) => MethodResult
+
+type PathMethodResult<
+ PathInfo extends Record,
+ Method extends keyof PathInfo,
+ Init,
+ Media extends MediaType
> = Effect.Effect<
- | ApiSuccess>
- | HttpError>,
- Exclude<
- ApiFailure>,
- HttpError>
- >,
- HttpClient.HttpClient
+ FetchResponse, Init, Media>,
+ Error
>
-type DispatcherFor<
- Paths extends object,
- Path extends keyof Paths,
- Method extends HttpMethod
-> = Dispatcher>
+export type ClientForPath, Media extends MediaType> = {
+ [Method in keyof PathInfo as Uppercase]: <
+ Init extends MaybeOptionalInit
+ >(
+ ...init: InitParam
+ ) => PathMethodResult
+}
-type RequestOptionsForOperation<
- Paths extends object,
- Path extends keyof Paths,
- Method extends HttpMethod
-> = RequestOptionsFor>
-
-/**
- * Type-safe API client with full request-side type enforcement
- *
- * **Key guarantees:**
- * 1. GET only works on paths that have `get` method in schema
- * 2. POST only works on paths that have `post` method in schema
- * 3. Dispatcher type is derived from operation's responses
- * 4. Request options (params/query/body) are derived from operation
- *
- * **Effect Channel Design:**
- * - Success channel: `ApiSuccess` - 2xx responses only
- * - Error channel: `ApiFailure` - HTTP errors (4xx, 5xx) + boundary errors
- *
- * @typeParam Paths - OpenAPI paths type from openapi-typescript
- *
- * @pure false - operations perform HTTP requests
- * @invariant ∀ call: path ∈ PathsForMethod ∧ options derived from operation
- */
-export type StrictApiClient = {
- /**
- * Execute GET request
- *
- * @typeParam Path - Path that supports GET method (enforced at type level)
- * @param path - API path with GET method
- * @param dispatcher - Response dispatcher (must match operation responses)
- * @param options - Request options (typed from operation)
- * @returns Effect with 2xx in success channel, errors in error channel
- */
- readonly GET: >(
- path: Path,
- dispatcher: DispatcherFor,
- options?: RequestOptionsForOperation
- ) => RequestEffect
-
- /**
- * Execute POST request
- */
- readonly POST: >(
- path: Path,
- dispatcher: DispatcherFor,
- options?: RequestOptionsForOperation
- ) => RequestEffect
-
- /**
- * Execute PUT request
- */
- readonly PUT: >(
- path: Path,
- dispatcher: DispatcherFor,
- options?: RequestOptionsForOperation
- ) => RequestEffect
-
- /**
- * Execute DELETE request
- */
- readonly DELETE: >(
- path: Path,
- dispatcher: DispatcherFor,
- options?: RequestOptionsForOperation
- ) => RequestEffect
-
- /**
- * Execute PATCH request
- */
- readonly PATCH: >(
- path: Path,
- dispatcher: DispatcherFor,
- options?: RequestOptionsForOperation
- ) => RequestEffect
-
- /**
- * Execute HEAD request
- */
- readonly HEAD: >(
- path: Path,
- dispatcher: DispatcherFor,
- options?: RequestOptionsForOperation
- ) => RequestEffect
-
- /**
- * Execute OPTIONS request
- */
- readonly OPTIONS: >(
- path: Path,
- dispatcher: DispatcherFor,
- options?: RequestOptionsForOperation
- ) => RequestEffect
+export interface Client {
+ request: ClientRequestMethod
+ GET: ClientMethod
+ PUT: ClientMethod
+ POST: ClientMethod
+ DELETE: ClientMethod
+ OPTIONS: ClientMethod
+ HEAD: ClientMethod
+ PATCH: ClientMethod
+ TRACE: ClientMethod
+ use(...middleware: Array): void
+ eject(...middleware: Array): void
}
-/**
- * Type-safe API client with auto-dispatching (dispatcher is derived from path+method)
- *
- * **Key guarantees:**
- * 1. GET only works on paths that have `get` method in schema
- * 2. Dispatcher is looked up from provided dispatcher map by path+method
- * 3. Request options (params/query/body) are derived from operation
- *
- * **Effect Channel Design:**
- * - Success channel: `ApiSuccess` - 2xx responses only
- * - Error channel: `ApiFailure` - HTTP errors (4xx, 5xx) + boundary errors
- *
- * @typeParam Paths - OpenAPI paths type from openapi-typescript
- *
- * @pure false - operations perform HTTP requests
- * @invariant ∀ call: path ∈ PathsForMethod ∧ dispatcherMap[path][method] defined
- */
-export type StrictApiClientWithDispatchers = {
- /**
- * Execute GET request (dispatcher is inferred)
- */
- readonly GET: >(
- path: Path,
- options?: RequestOptionsForOperation
- ) => RequestEffect
-
- /**
- * Execute POST request (dispatcher is inferred)
- */
- readonly POST: >(
- path: Path,
- options?: RequestOptionsForOperation
- ) => RequestEffect
-
- /**
- * Execute PUT request (dispatcher is inferred)
- */
- readonly PUT: >(
- path: Path,
- options?: RequestOptionsForOperation
- ) => RequestEffect
-
- /**
- * Execute DELETE request (dispatcher is inferred)
- */
- readonly DELETE: >(
- path: Path,
- options?: RequestOptionsForOperation
- ) => RequestEffect
-
- /**
- * Execute PATCH request (dispatcher is inferred)
- */
- readonly PATCH: