vovk 3.5.1 → 4.0.0-beta.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.
- package/CHANGELOG.md +74 -0
- package/dist/client/create-rpc.d.ts +13 -0
- package/dist/client/create-rpc.js +145 -0
- package/dist/client/default-handler.d.ts +6 -0
- package/dist/client/default-handler.js +25 -0
- package/dist/client/default-stream-handler.d.ts +12 -0
- package/dist/client/default-stream-handler.js +290 -0
- package/dist/client/fetcher.d.ts +2 -2
- package/dist/client/fetcher.js +54 -5
- package/dist/client/progressive.d.ts +1 -5
- package/dist/client/progressive.js +19 -9
- package/dist/client/serialize-query.d.ts +2 -0
- package/dist/client/serialize-query.js +38 -0
- package/dist/core/apply-decorator-adapter.d.ts +7 -0
- package/dist/core/apply-decorator-adapter.js +50 -0
- package/dist/core/controllers-to-static-params.d.ts +12 -0
- package/dist/core/controllers-to-static-params.js +31 -0
- package/dist/core/create-decorator.d.ts +12 -0
- package/dist/core/create-decorator.js +52 -0
- package/dist/core/decorate.d.ts +2 -6
- package/dist/core/decorators.js +4 -4
- package/dist/core/get-schema.d.ts +21 -0
- package/dist/core/get-schema.js +29 -0
- package/dist/core/http-exception.d.ts +16 -0
- package/dist/core/http-exception.js +26 -0
- package/dist/core/init-segment.d.ts +33 -0
- package/dist/core/init-segment.js +62 -0
- package/dist/core/json-lines-responder.d.ts +38 -0
- package/dist/core/json-lines-responder.js +116 -0
- package/dist/core/resolve-generator-config-values.d.ts +19 -0
- package/dist/core/resolve-generator-config-values.js +59 -0
- package/dist/core/set-handler-schema.d.ts +4 -0
- package/dist/core/set-handler-schema.js +12 -0
- package/dist/core/to-download-response.d.ts +11 -0
- package/dist/core/to-download-response.js +25 -0
- package/dist/core/vovk-app.d.ts +36 -0
- package/dist/core/vovk-app.js +364 -0
- package/dist/index.d.ts +19 -21
- package/dist/index.js +16 -17
- package/dist/internal.d.ts +16 -14
- package/dist/internal.js +10 -9
- package/dist/openapi/error.js +2 -2
- package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.d.ts +12 -0
- package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.js +84 -0
- package/dist/openapi/openapi-to-vovk-schema/index.d.ts +5 -0
- package/dist/openapi/openapi-to-vovk-schema/index.js +193 -0
- package/dist/openapi/openapi-to-vovk-schema/inline-refs.d.ts +2 -0
- package/dist/openapi/openapi-to-vovk-schema/inline-refs.js +88 -0
- package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.d.ts +2 -0
- package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.js +46 -0
- package/dist/openapi/operation.js +1 -1
- package/dist/openapi/tool.js +1 -1
- package/dist/openapi/vovk-schema-to-openapi.d.ts +21 -0
- package/dist/openapi/vovk-schema-to-openapi.js +256 -0
- package/dist/req/buffer-body.d.ts +1 -0
- package/dist/req/buffer-body.js +30 -0
- package/dist/req/parse-body.d.ts +4 -0
- package/dist/req/parse-body.js +49 -0
- package/dist/req/parse-form.d.ts +1 -0
- package/dist/req/parse-form.js +24 -0
- package/dist/req/parse-query.d.ts +1 -0
- package/dist/req/parse-query.js +164 -0
- package/dist/req/req-meta.d.ts +2 -0
- package/dist/req/req-meta.js +10 -0
- package/dist/req/req-query.d.ts +2 -0
- package/dist/req/req-query.js +4 -0
- package/dist/req/validate-content-type.d.ts +1 -0
- package/dist/req/validate-content-type.js +32 -0
- package/dist/samples/create-code-samples.d.ts +20 -0
- package/dist/samples/create-code-samples.js +294 -0
- package/dist/samples/object-to-code.d.ts +8 -0
- package/dist/samples/object-to-code.js +38 -0
- package/dist/samples/schema-to-code.d.ts +11 -0
- package/dist/samples/schema-to-code.js +267 -0
- package/dist/samples/schema-to-object.d.ts +2 -0
- package/dist/samples/schema-to-object.js +167 -0
- package/dist/samples/schema-to-ts-type.d.ts +2 -0
- package/dist/samples/schema-to-ts-type.js +114 -0
- package/dist/tools/create-tool-factory.d.ts +135 -0
- package/dist/tools/create-tool-factory.js +62 -0
- package/dist/tools/create-tool.d.ts +126 -0
- package/dist/tools/create-tool.js +6 -0
- package/dist/tools/derive-tools.d.ts +42 -0
- package/dist/tools/derive-tools.js +132 -0
- package/dist/tools/to-model-output-default.d.ts +7 -0
- package/dist/tools/to-model-output-default.js +21 -0
- package/dist/tools/to-model-output-mcp.d.ts +31 -0
- package/dist/tools/to-model-output-mcp.js +54 -0
- package/dist/tools/to-model-output.d.ts +8 -0
- package/dist/tools/to-model-output.js +10 -0
- package/dist/types/client.d.ts +5 -5
- package/dist/types/config.d.ts +5 -6
- package/dist/types/core.d.ts +4 -1
- package/dist/types/enums.d.ts +1 -1
- package/dist/types/enums.js +1 -1
- package/dist/types/inference.d.ts +3 -4
- package/dist/types/request.d.ts +1 -5
- package/dist/types/standard-schema.js +0 -2
- package/dist/types/standard-tool.d.ts +8 -12
- package/dist/types/tools.d.ts +2 -40
- package/dist/types/validation.d.ts +3 -3
- package/dist/utils/camel-case.d.ts +1 -0
- package/dist/utils/camel-case.js +26 -0
- package/dist/utils/deep-extend.d.ts +45 -0
- package/dist/utils/deep-extend.js +139 -0
- package/dist/utils/file-name-to-disposition.d.ts +1 -0
- package/dist/utils/file-name-to-disposition.js +3 -0
- package/dist/utils/to-kebab-case.d.ts +1 -0
- package/dist/utils/to-kebab-case.js +5 -0
- package/dist/utils/trim-path.d.ts +1 -0
- package/dist/utils/trim-path.js +1 -0
- package/dist/utils/upper-first.d.ts +1 -0
- package/dist/utils/upper-first.js +3 -0
- package/dist/validation/create-standard-validation.d.ts +145 -0
- package/dist/validation/create-standard-validation.js +44 -0
- package/dist/validation/create-validate-on-client.d.ts +14 -0
- package/dist/validation/create-validate-on-client.js +23 -0
- package/dist/validation/json-schema-only-spec.d.ts +14 -0
- package/dist/validation/json-schema-only-spec.js +57 -0
- package/dist/validation/procedure.d.ts +1 -124
- package/dist/validation/procedure.js +1 -1
- package/dist/validation/validation-schemas-object-to-single-validation-schema.d.ts +8 -0
- package/dist/validation/validation-schemas-object-to-single-validation-schema.js +89 -0
- package/dist/validation/with-validation-library.d.ts +119 -0
- package/dist/validation/with-validation-library.js +190 -0
- package/package.json +9 -8
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `vovk` are documented here. This file is the canonical record: noteworthy changes only, one short line each, linked to the pull request or commit that made them. GitHub Releases are not used.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## 4.0.0-beta.0 - unreleased
|
|
8
|
+
|
|
9
|
+
A cleanup major: removals and renames, no new APIs.
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- `toolsByName`: `deriveTools` returns the tools array only ([#28](https://github.com/finom/vovk/pull/28))
|
|
14
|
+
- `VovkTool`, `createTool`, `parameters`, `type` and `inputSchemas`: replaced by the `StandardToolV0` convention ([#27](https://github.com/finom/vovk/pull/27))
|
|
15
|
+
- `vovk/createRPC` and `vovk/createValidateOnClient` subpath aliases: use `vovk/create-rpc` and `vovk/create-validate-on-client` ([b7dc4b2](https://github.com/finom/vovk/commit/b7dc4b2a))
|
|
16
|
+
- Dead `nestjs-operation-id` name-strategy literals ([3f84c25](https://github.com/finom/vovk/commit/3f84c253))
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `HttpStatus.TOO_MANY_TRequestS` renamed to `TOO_MANY_REQUESTS` ([c739e4b](https://github.com/finom/vovk/commit/c739e4bc))
|
|
21
|
+
- Production error responses carry no internal detail; `onError` still receives the full error ([d0be248](https://github.com/finom/vovk/commit/d0be2482), [419cf7f](https://github.com/finom/vovk/commit/419cf7f6))
|
|
22
|
+
- A declared `contentType` is enforced even without a body schema; disabling body validation opts out ([444644b](https://github.com/finom/vovk/commit/444644b8), [27edb95](https://github.com/finom/vovk/commit/27edb95e))
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Stacked decorators all run: HTTP routes dispatch through the outermost wrapper ([18ce506](https://github.com/finom/vovk/commit/18ce5063))
|
|
27
|
+
- Prototype members such as `constructor` no longer resolve as route handlers ([8fa67bb](https://github.com/finom/vovk/commit/8fa67bb0))
|
|
28
|
+
- The route match cache is scoped to its handlers map, fixing cross-method poisoning ([6db95ff](https://github.com/finom/vovk/commit/6db95ff5))
|
|
29
|
+
- Path params are percent-encoded, and an encoded slash stays inside its param ([d2d9046](https://github.com/finom/vovk/commit/d2d90464), [0cb5aa4](https://github.com/finom/vovk/commit/0cb5aa40))
|
|
30
|
+
- Chained `withDefaults` deep-merges instead of replacing nested options ([4f53fc7](https://github.com/finom/vovk/commit/4f53fc78))
|
|
31
|
+
- `Headers` instances and `init.signal` survive RPC options ([fc61920](https://github.com/finom/vovk/commit/fc619204))
|
|
32
|
+
- Streaming: mid-stream errors reach `onError`, the error envelope stays out of `onIterate`, an abandoned iterator releases the stream ([b440310](https://github.com/finom/vovk/commit/b4403106), [06e99fa](https://github.com/finom/vovk/commit/06e99faa), [06b9c00](https://github.com/finom/vovk/commit/06b9c007))
|
|
33
|
+
- Falsy handler output (`false`, `0`, `''`, `null`) is accepted when an output schema is set ([dc8c6a3](https://github.com/finom/vovk/commit/dc8c6a38))
|
|
34
|
+
- Derived tools materialize `Response` and generator results, and failures reach `onError` ([7b7f1b8](https://github.com/finom/vovk/commit/7b7f1b87), [ff18ee5](https://github.com/finom/vovk/commit/ff18ee5d))
|
|
35
|
+
- Derived path/query parameters and `components.schemas` merge with user-declared ones ([d86bae0](https://github.com/finom/vovk/commit/d86bae0b), [6a174ac](https://github.com/finom/vovk/commit/6a174ac1))
|
|
36
|
+
- Malformed `x-meta` responds 400, `x-meta` is allowed in default CORS headers, non-ASCII meta is escaped ([5327a32](https://github.com/finom/vovk/commit/5327a327), [e34acae](https://github.com/finom/vovk/commit/e34acaea), [be58398](https://github.com/finom/vovk/commit/be583983))
|
|
37
|
+
|
|
38
|
+
### Security
|
|
39
|
+
|
|
40
|
+
- `x-tsType` is stripped from third-party OpenAPI specs on ingestion; a crafted value could inject executable code into the generated client ([c5e63cd](https://github.com/finom/vovk/commit/c5e63cd7))
|
|
41
|
+
- Query parsing hardened: prototype-polluting keys dropped, pairs split at the first `=`, a large index cannot size a huge array ([46aaadf](https://github.com/finom/vovk/commit/46aaadf7), [03522f3](https://github.com/finom/vovk/commit/03522f3f), [14bbc17](https://github.com/finom/vovk/commit/14bbc179))
|
|
42
|
+
- A non-index bracket key such as `?a[-1]=x` no longer discards the value; it becomes an object key, matching `qs`
|
|
43
|
+
|
|
44
|
+
### Upgrading from 3.x
|
|
45
|
+
|
|
46
|
+
1. Replace `HttpStatus.TOO_MANY_TRequestS` with `HttpStatus.TOO_MANY_REQUESTS`.
|
|
47
|
+
2. Replace imports from `vovk/createRPC` and `vovk/createValidateOnClient` with `vovk/create-rpc` and `vovk/create-validate-on-client`.
|
|
48
|
+
3. If you destructured `toolsByName` from `deriveTools`, build the map yourself from the returned array.
|
|
49
|
+
4. If you used `createTool` / `VovkTool` / `inputSchemas`, move to the `StandardToolV0` shape.
|
|
50
|
+
5. If you relied on error responses carrying internal messages in production, read them from `onError` instead.
|
|
51
|
+
6. The composed client no longer comes from the `vovk-client` package; see the `vovk-cli` changelog.
|
|
52
|
+
|
|
53
|
+
## 3.7.0 - 2026-06-11
|
|
54
|
+
|
|
55
|
+
- Kebab-case file naming and subpath exports across packages; camelCase aliases such as `vovk/createRPC` kept for compatibility ([#23](https://github.com/finom/vovk/pull/23))
|
|
56
|
+
|
|
57
|
+
## 3.5.0 - 2026-06-10
|
|
58
|
+
|
|
59
|
+
- `openAPIMixin.filterOperations` and `pruneComponents`: generate only the operations you call and drop components nothing references ([#22](https://github.com/finom/vovk/pull/22))
|
|
60
|
+
|
|
61
|
+
## 3.4.0 - 2026-05-30
|
|
62
|
+
|
|
63
|
+
- Merged `inputSchema`, one Standard Schema for body, query and params; per-slot `inputSchemas` deprecated ([#16](https://github.com/finom/vovk/pull/16))
|
|
64
|
+
- `VovkTool` follows the `standard-tool` convention, and `execute` takes per-call `meta` ([#18](https://github.com/finom/vovk/pull/18), [#19](https://github.com/finom/vovk/pull/19), [#20](https://github.com/finom/vovk/pull/20))
|
|
65
|
+
|
|
66
|
+
## 3.2.2 - 2026-04-03
|
|
67
|
+
|
|
68
|
+
- Fetcher `onSuccess`/`onError` assignable after initialization ([#3](https://github.com/finom/vovk/pull/3)), the `VovkInput` type ([#1](https://github.com/finom/vovk/pull/1)), and `.fn` LPC calls inside Next.js server actions ([#2](https://github.com/finom/vovk/pull/2))
|
|
69
|
+
|
|
70
|
+
## 3.1.3 - 2026-03-21
|
|
71
|
+
|
|
72
|
+
- `decorate()` and `static prefix`: controllers and procedures without decorator syntax ([docs](https://vovk.dev/decorator-overview))
|
|
73
|
+
|
|
74
|
+
Earlier history predates this changelog; see the git tags.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { VovkHandlerSchema } from '../internal.js';
|
|
2
|
+
import type { VovkFetcher, VovkFetcherOptions, VovkRPCModule } from '../types/client.js';
|
|
3
|
+
import type { VovkRequest } from '../types/request.js';
|
|
4
|
+
import type { KnownAny } from '../types/utils.js';
|
|
5
|
+
import type { CombinedSpec } from '../types/validation.js';
|
|
6
|
+
export type { CombinedSpec, VovkHandlerSchema, VovkRequest };
|
|
7
|
+
/**
|
|
8
|
+
* Creates a client-side RPC module for interacting with server-side controllers.
|
|
9
|
+
* @see https://vovk.dev/typescript
|
|
10
|
+
*/
|
|
11
|
+
export declare const createRPC: <T, OPTS extends Record<string, KnownAny> = Record<string, never>>(givenSchema: unknown, segmentName: string, rpcModuleName: string, givenFetcher?: VovkFetcher<OPTS> | Promise<{
|
|
12
|
+
fetcher: VovkFetcher<OPTS>;
|
|
13
|
+
}>, options?: VovkFetcherOptions<OPTS>) => VovkRPCModule<T, OPTS>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { deepExtend } from '../utils/deep-extend.js';
|
|
2
|
+
import { defaultHandler } from './default-handler.js';
|
|
3
|
+
import { defaultStreamHandler } from './default-stream-handler.js';
|
|
4
|
+
import { fetcher as defaultFetcher } from './fetcher.js';
|
|
5
|
+
import { serializeQuery } from './serialize-query.js';
|
|
6
|
+
const trimPath = (path) => path.trim().replace(/^\/|\/$/g, '');
|
|
7
|
+
const getHandlerPath = (endpoint, params, query) => {
|
|
8
|
+
let result = endpoint;
|
|
9
|
+
const queryStr = query ? serializeQuery(query) : null;
|
|
10
|
+
for (const [key, value] of Object.entries(params ?? {})) {
|
|
11
|
+
// encode so a value stays one path segment, the callback form also keeps $& from being a replacement pattern
|
|
12
|
+
result = result.replaceAll(`{${key}}`, () => encodeURIComponent(String(value)));
|
|
13
|
+
}
|
|
14
|
+
return `${result}${queryStr ? `?${queryStr}` : ''}`;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Creates a client-side RPC module for interacting with server-side controllers.
|
|
18
|
+
* @see https://vovk.dev/typescript
|
|
19
|
+
*/
|
|
20
|
+
export const createRPC = (givenSchema, segmentName, rpcModuleName, givenFetcher, options) => {
|
|
21
|
+
const schema = givenSchema; // fixes incompatibilities with JSON module
|
|
22
|
+
// fetcher ??= defaultFetcher as NonNullable<typeof fetcher>;
|
|
23
|
+
const segmentNamePath = options?.segmentNameOverride ?? segmentName;
|
|
24
|
+
const segmentSchema = schema.segments[segmentName];
|
|
25
|
+
if (!segmentSchema)
|
|
26
|
+
throw new Error(`Unable to create RPC module. Segment schema is missing for segment "${segmentName}".`);
|
|
27
|
+
let controllerSchema = schema.segments[segmentName]?.controllers[rpcModuleName];
|
|
28
|
+
const client = {};
|
|
29
|
+
if (!controllerSchema) {
|
|
30
|
+
console.warn(`🐺 Unable to create RPC module. Controller schema is missing for module "${rpcModuleName}" from segment "${segmentName}". Assuming that schema is not ready yet and a segment is importing an uncompiled RPC module.`);
|
|
31
|
+
controllerSchema = {
|
|
32
|
+
rpcModuleName,
|
|
33
|
+
prefix: '',
|
|
34
|
+
handlers: {},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const controllerPrefix = trimPath(controllerSchema.prefix ?? '');
|
|
38
|
+
const forceApiRoot = segmentSchema.forceApiRoot;
|
|
39
|
+
const configRootEntry = schema.meta?.config?.rootEntry;
|
|
40
|
+
const originalApiRoot = forceApiRoot ?? options?.apiRoot ?? (configRootEntry ? `/${configRootEntry}` : '/api');
|
|
41
|
+
for (const [staticMethodName, handlerSchema] of Object.entries(controllerSchema.handlers ?? {})) {
|
|
42
|
+
const { path, httpMethod, validation } = handlerSchema;
|
|
43
|
+
const getURL = ({ apiRoot, params, query } = {}) => {
|
|
44
|
+
apiRoot = apiRoot ?? originalApiRoot;
|
|
45
|
+
const endpoint = [
|
|
46
|
+
apiRoot.startsWith('http://') || apiRoot.startsWith('https://') || apiRoot.startsWith('/') ? '' : '/',
|
|
47
|
+
apiRoot,
|
|
48
|
+
forceApiRoot ? '' : segmentNamePath,
|
|
49
|
+
getHandlerPath([controllerPrefix, path].filter(Boolean).join('/'), params, query),
|
|
50
|
+
]
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.join('/')
|
|
53
|
+
.replace(/([^:])\/+/g, '$1/'); // replace // by / but not for protocols (http://, https://)
|
|
54
|
+
return endpoint;
|
|
55
|
+
};
|
|
56
|
+
const handler = (async (input = {}) => {
|
|
57
|
+
const optionsResolvedValidateOnClient = options?.validateOnClient instanceof Promise
|
|
58
|
+
? (await options?.validateOnClient)?.validateOnClient
|
|
59
|
+
: options?.validateOnClient;
|
|
60
|
+
const fetcher = givenFetcher instanceof Promise
|
|
61
|
+
? (await givenFetcher).fetcher
|
|
62
|
+
: (givenFetcher ?? defaultFetcher);
|
|
63
|
+
const validate = async (validationInput, { endpoint, }) => {
|
|
64
|
+
const validateOnClient = input.validateOnClient ?? optionsResolvedValidateOnClient;
|
|
65
|
+
if (validateOnClient && validation) {
|
|
66
|
+
if (typeof validateOnClient !== 'function') {
|
|
67
|
+
throw new Error('validateOnClient must be a function');
|
|
68
|
+
}
|
|
69
|
+
return ((await validateOnClient({ ...validationInput }, validation, { fullSchema: schema, endpoint })) ??
|
|
70
|
+
validationInput);
|
|
71
|
+
}
|
|
72
|
+
return validationInput;
|
|
73
|
+
};
|
|
74
|
+
const internalOptions = {
|
|
75
|
+
name: staticMethodName,
|
|
76
|
+
httpMethod: httpMethod,
|
|
77
|
+
getURL,
|
|
78
|
+
validate,
|
|
79
|
+
defaultHandler,
|
|
80
|
+
defaultStreamHandler,
|
|
81
|
+
schema: handlerSchema,
|
|
82
|
+
};
|
|
83
|
+
let processedBody = input.body;
|
|
84
|
+
if ((validation?.body?.['x-contentType']?.includes('multipart/form-data') ||
|
|
85
|
+
validation?.body?.['x-contentType']?.includes('application/x-www-form-urlencoded')) &&
|
|
86
|
+
input.body &&
|
|
87
|
+
!(input.body instanceof FormData || input.body instanceof URLSearchParams || input.body instanceof Blob)) {
|
|
88
|
+
processedBody = new FormData();
|
|
89
|
+
for (const [key, value] of Object.entries(input.body)) {
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
value.forEach((item) => {
|
|
92
|
+
processedBody.append(key, item);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
processedBody.append(key, value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
processedBody = input.body;
|
|
102
|
+
}
|
|
103
|
+
const internalInput = {
|
|
104
|
+
...deepExtend({}, options, {
|
|
105
|
+
validateOnClient: optionsResolvedValidateOnClient,
|
|
106
|
+
}, input),
|
|
107
|
+
body: processedBody ?? null,
|
|
108
|
+
query: input.query ?? {},
|
|
109
|
+
params: input.params ?? {},
|
|
110
|
+
};
|
|
111
|
+
if (!fetcher)
|
|
112
|
+
throw new Error('Fetcher is not provided');
|
|
113
|
+
const [respData, resp] = await fetcher(internalOptions, internalInput);
|
|
114
|
+
return input.transform ? input.transform(respData, resp) : respData;
|
|
115
|
+
});
|
|
116
|
+
// TODO use Object.freeze, Object.seal or Object.defineProperty to avoid mutation
|
|
117
|
+
handler.schema = handlerSchema;
|
|
118
|
+
handler.controllerSchema = controllerSchema;
|
|
119
|
+
handler.segmentSchema = segmentSchema;
|
|
120
|
+
handler.fullSchema = schema;
|
|
121
|
+
handler.isRPC = true;
|
|
122
|
+
handler.apiRoot = originalApiRoot;
|
|
123
|
+
handler.getURL = getURL;
|
|
124
|
+
handler.queryKey = (key) => [
|
|
125
|
+
handler.segmentSchema.segmentName,
|
|
126
|
+
handler.controllerSchema.prefix ?? '',
|
|
127
|
+
handler.controllerSchema.rpcModuleName,
|
|
128
|
+
handler.schema.path,
|
|
129
|
+
handler.schema.httpMethod,
|
|
130
|
+
...(key ?? []),
|
|
131
|
+
];
|
|
132
|
+
// @ts-expect-error TODO
|
|
133
|
+
client[staticMethodName] = handler;
|
|
134
|
+
}
|
|
135
|
+
Object.defineProperty(client, 'withDefaults', {
|
|
136
|
+
value: (newOptions) => {
|
|
137
|
+
// deep merge to match per-call option merging, so chained defaults don't clobber nested keys
|
|
138
|
+
return createRPC(schema, segmentName, rpcModuleName, givenFetcher, deepExtend({}, options, newOptions));
|
|
139
|
+
},
|
|
140
|
+
enumerable: false,
|
|
141
|
+
writable: false,
|
|
142
|
+
configurable: false,
|
|
143
|
+
});
|
|
144
|
+
return client;
|
|
145
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { VovkHandlerSchema } from '../types/core.js';
|
|
2
|
+
export declare const DEFAULT_ERROR_MESSAGE = "Unknown error at defaultHandler";
|
|
3
|
+
export declare const defaultHandler: ({ response, schema }: {
|
|
4
|
+
response: Response;
|
|
5
|
+
schema: VovkHandlerSchema;
|
|
6
|
+
}) => Promise<unknown>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { HttpException } from '../core/http-exception.js';
|
|
2
|
+
export const DEFAULT_ERROR_MESSAGE = 'Unknown error at defaultHandler';
|
|
3
|
+
// Helper function to get a value from an object using dot notation path
|
|
4
|
+
const getNestedValue = (obj, path) => {
|
|
5
|
+
return path.split('.').reduce((o, key) => (o && typeof o === 'object' ? o[key] : undefined), obj);
|
|
6
|
+
};
|
|
7
|
+
export const defaultHandler = async ({ response, schema }) => {
|
|
8
|
+
let result;
|
|
9
|
+
try {
|
|
10
|
+
result = await response.json();
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
// handle parsing errors
|
|
14
|
+
throw new HttpException(response.status, e?.message ?? DEFAULT_ERROR_MESSAGE);
|
|
15
|
+
}
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
const errorKey = schema.operationObject && 'x-errorMessageKey' in schema.operationObject
|
|
18
|
+
? schema.operationObject['x-errorMessageKey']
|
|
19
|
+
: 'message';
|
|
20
|
+
// handle server errors
|
|
21
|
+
const errorResponse = result;
|
|
22
|
+
throw new HttpException(response.status, getNestedValue(errorResponse, errorKey) ?? DEFAULT_ERROR_MESSAGE, errorResponse?.cause ?? JSON.stringify(result));
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { VovkStreamAsyncIterable } from '../types/client.js';
|
|
2
|
+
import '../utils/shim.js';
|
|
3
|
+
export declare const DEFAULT_ERROR_MESSAGE = "An unknown error at the default stream handler";
|
|
4
|
+
/** ReadableStream of JSON Lines to VovkStreamAsyncIterable, reusable outside HTTP contexts. @see https://vovk.dev/jsonlines */
|
|
5
|
+
export declare const readableStreamToAsyncIterable: <T = unknown>({ readableStream, abortController, }: {
|
|
6
|
+
readableStream: ReadableStream<Uint8Array | string>;
|
|
7
|
+
abortController?: AbortController;
|
|
8
|
+
}) => Omit<VovkStreamAsyncIterable<T>, "abortController" | "status">;
|
|
9
|
+
export declare const defaultStreamHandler: ({ response, abortController, }: {
|
|
10
|
+
response: Response;
|
|
11
|
+
abortController: AbortController;
|
|
12
|
+
}) => VovkStreamAsyncIterable<unknown>;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { HttpException } from '../core/http-exception.js';
|
|
2
|
+
import { HttpStatus } from '../types/enums.js';
|
|
3
|
+
import '../utils/shim.js';
|
|
4
|
+
export const DEFAULT_ERROR_MESSAGE = 'An unknown error at the default stream handler';
|
|
5
|
+
/** ReadableStream of JSON Lines to VovkStreamAsyncIterable, reusable outside HTTP contexts. @see https://vovk.dev/jsonlines */
|
|
6
|
+
export const readableStreamToAsyncIterable = ({ readableStream, abortController, }) => {
|
|
7
|
+
const reader = readableStream.getReader();
|
|
8
|
+
const subscribers = new Set();
|
|
9
|
+
// State
|
|
10
|
+
let isAbortedWithoutError = false;
|
|
11
|
+
let streamExhausted = false;
|
|
12
|
+
let streamError = null;
|
|
13
|
+
let errorIndex = -1;
|
|
14
|
+
let primaryStarted = false;
|
|
15
|
+
// consumers currently iterating, the last one to leave early releases the connection
|
|
16
|
+
let activeIterators = 0;
|
|
17
|
+
const cachedItems = [];
|
|
18
|
+
const waiters = [];
|
|
19
|
+
// --- Helper functions ---
|
|
20
|
+
const notifyWaiters = () => {
|
|
21
|
+
for (let i = waiters.length - 1; i >= 0; i--) {
|
|
22
|
+
const waiter = waiters[i];
|
|
23
|
+
let handled = false;
|
|
24
|
+
if (streamError && waiter.index >= errorIndex) {
|
|
25
|
+
waiter.reject(streamError);
|
|
26
|
+
handled = true;
|
|
27
|
+
}
|
|
28
|
+
else if (waiter.index < cachedItems.length) {
|
|
29
|
+
waiter.resolve({ value: cachedItems[waiter.index], done: false });
|
|
30
|
+
handled = true;
|
|
31
|
+
}
|
|
32
|
+
else if (streamExhausted || (abortController?.signal.aborted && isAbortedWithoutError)) {
|
|
33
|
+
waiter.resolve({ value: undefined, done: true });
|
|
34
|
+
handled = true;
|
|
35
|
+
}
|
|
36
|
+
if (handled) {
|
|
37
|
+
waiters.splice(i, 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const setStreamError = (error) => {
|
|
42
|
+
errorIndex = cachedItems.length;
|
|
43
|
+
streamError = error;
|
|
44
|
+
notifyWaiters();
|
|
45
|
+
};
|
|
46
|
+
const disposeStream = (reason) => {
|
|
47
|
+
isAbortedWithoutError = true;
|
|
48
|
+
streamExhausted = true;
|
|
49
|
+
notifyWaiters();
|
|
50
|
+
abortController?.abort(reason);
|
|
51
|
+
reader.cancel().catch(() => { });
|
|
52
|
+
};
|
|
53
|
+
// --- Primary reader ---
|
|
54
|
+
const runPrimaryReader = async () => {
|
|
55
|
+
let buffer = '';
|
|
56
|
+
let iterationIndex = 0;
|
|
57
|
+
// Returns true if the stream should stop (error encountered)
|
|
58
|
+
const processLine = (line) => {
|
|
59
|
+
let data;
|
|
60
|
+
try {
|
|
61
|
+
data = JSON.parse(line);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (data) {
|
|
67
|
+
// the error envelope is a control message, not data, subscribers must not see it
|
|
68
|
+
if (typeof data === 'object' && data !== null && 'isError' in data && 'reason' in data) {
|
|
69
|
+
const upcomingError = data.reason;
|
|
70
|
+
abortController?.abort(upcomingError);
|
|
71
|
+
const error = typeof upcomingError === 'string' ? new Error(upcomingError) : upcomingError;
|
|
72
|
+
setStreamError(error);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
subscribers.forEach((cb) => {
|
|
76
|
+
if (!abortController?.signal.aborted)
|
|
77
|
+
cb(data, iterationIndex);
|
|
78
|
+
});
|
|
79
|
+
iterationIndex++;
|
|
80
|
+
if (!abortController?.signal.aborted) {
|
|
81
|
+
cachedItems.push(data);
|
|
82
|
+
notifyWaiters();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
while (true) {
|
|
89
|
+
if (abortController?.signal.aborted && isAbortedWithoutError) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
let value;
|
|
93
|
+
let done;
|
|
94
|
+
try {
|
|
95
|
+
({ value, done } = await reader.read());
|
|
96
|
+
if (done)
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error?.name === 'AbortError' && isAbortedWithoutError) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const err = new Error(`JSONLines stream error. ${String(error)}`);
|
|
104
|
+
err.cause = error;
|
|
105
|
+
setStreamError(err);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const chunk = typeof value === 'string'
|
|
109
|
+
? value
|
|
110
|
+
: typeof value === 'number'
|
|
111
|
+
? String.fromCharCode(value)
|
|
112
|
+
: new TextDecoder().decode(value);
|
|
113
|
+
buffer += chunk;
|
|
114
|
+
let newlineIdx;
|
|
115
|
+
while (true) {
|
|
116
|
+
newlineIdx = buffer.indexOf('\n');
|
|
117
|
+
if (newlineIdx === -1)
|
|
118
|
+
break;
|
|
119
|
+
if (abortController?.signal.aborted && isAbortedWithoutError) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
const line = buffer.slice(0, newlineIdx);
|
|
123
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
124
|
+
if (!line)
|
|
125
|
+
continue;
|
|
126
|
+
if (processLine(line))
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (abortController?.signal.aborted && isAbortedWithoutError) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Process any remaining data in the buffer (last line without trailing newline)
|
|
134
|
+
const remaining = buffer.trim();
|
|
135
|
+
if (remaining) {
|
|
136
|
+
processLine(remaining);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
streamExhausted = true;
|
|
141
|
+
notifyWaiters();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
// --- Async iterator ---
|
|
145
|
+
async function* asyncIterator() {
|
|
146
|
+
if (!primaryStarted) {
|
|
147
|
+
primaryStarted = true;
|
|
148
|
+
void runPrimaryReader();
|
|
149
|
+
}
|
|
150
|
+
activeIterators++;
|
|
151
|
+
let index = 0;
|
|
152
|
+
try {
|
|
153
|
+
while (true) {
|
|
154
|
+
// Check error first
|
|
155
|
+
if (streamError && index >= errorIndex) {
|
|
156
|
+
throw streamError;
|
|
157
|
+
}
|
|
158
|
+
// Clean exit on abort without error
|
|
159
|
+
if (abortController?.signal.aborted && isAbortedWithoutError) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Yield from cache if available
|
|
163
|
+
if (index < cachedItems.length) {
|
|
164
|
+
yield cachedItems[index++];
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Stream finished
|
|
168
|
+
if (streamExhausted) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Wait for next item or completion
|
|
172
|
+
const result = await new Promise((resolve, reject) => {
|
|
173
|
+
// Re-check state inside promise to handle race conditions
|
|
174
|
+
if (streamError && index >= errorIndex) {
|
|
175
|
+
reject(streamError);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (abortController?.signal.aborted && isAbortedWithoutError) {
|
|
179
|
+
resolve({ value: undefined, done: true });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (index < cachedItems.length) {
|
|
183
|
+
resolve({ value: cachedItems[index], done: false });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (streamExhausted) {
|
|
187
|
+
resolve({ value: undefined, done: true });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
waiters.push({ index, resolve, reject });
|
|
191
|
+
});
|
|
192
|
+
if (result.done) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
index++;
|
|
196
|
+
yield result.value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
activeIterators--;
|
|
201
|
+
// a consumer that stopped early must release the connection, same as dispose does
|
|
202
|
+
if (activeIterators === 0 && !streamExhausted)
|
|
203
|
+
disposeStream('Stream iteration stopped');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// --- Public API ---
|
|
207
|
+
const asPromise = async () => {
|
|
208
|
+
const items = [];
|
|
209
|
+
for await (const item of asyncIterator()) {
|
|
210
|
+
items.push(item);
|
|
211
|
+
}
|
|
212
|
+
return items;
|
|
213
|
+
};
|
|
214
|
+
const abortSilently = (reason) => {
|
|
215
|
+
isAbortedWithoutError = true;
|
|
216
|
+
streamExhausted = true;
|
|
217
|
+
notifyWaiters();
|
|
218
|
+
abortController?.abort(reason);
|
|
219
|
+
reader.cancel().catch(() => { });
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
asPromise,
|
|
223
|
+
// abortController,
|
|
224
|
+
[Symbol.asyncIterator]: asyncIterator,
|
|
225
|
+
[Symbol.dispose]: () => disposeStream('Stream disposed'),
|
|
226
|
+
[Symbol.asyncDispose]: async () => disposeStream('Stream async disposed'),
|
|
227
|
+
abortSilently,
|
|
228
|
+
onIterate: (cb) => {
|
|
229
|
+
if (abortController?.signal.aborted)
|
|
230
|
+
return () => { };
|
|
231
|
+
subscribers.add(cb);
|
|
232
|
+
return () => subscribers.delete(cb);
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
export const defaultStreamHandler = ({ response, abortController, }) => {
|
|
237
|
+
// Handle error responses by creating a stream that fails on first iteration
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
let cachedError = null;
|
|
240
|
+
let errorParsed = false;
|
|
241
|
+
// Parse error asynchronously and cache it
|
|
242
|
+
void response
|
|
243
|
+
.json()
|
|
244
|
+
.then((res) => {
|
|
245
|
+
cachedError = new HttpException(response.status, res.message ?? DEFAULT_ERROR_MESSAGE);
|
|
246
|
+
})
|
|
247
|
+
.catch((e) => {
|
|
248
|
+
cachedError = new HttpException(response.status, e.message ?? DEFAULT_ERROR_MESSAGE, e);
|
|
249
|
+
})
|
|
250
|
+
.finally(() => {
|
|
251
|
+
errorParsed = true;
|
|
252
|
+
});
|
|
253
|
+
const getError = async () => {
|
|
254
|
+
// Wait for error to be parsed
|
|
255
|
+
while (!errorParsed) {
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
257
|
+
}
|
|
258
|
+
return cachedError ?? new HttpException(response.status, DEFAULT_ERROR_MESSAGE);
|
|
259
|
+
};
|
|
260
|
+
const errorIterator = () => ({
|
|
261
|
+
async next() {
|
|
262
|
+
throw await getError();
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const noop = () => { };
|
|
266
|
+
return {
|
|
267
|
+
status: response.status,
|
|
268
|
+
asPromise: async () => {
|
|
269
|
+
throw await getError();
|
|
270
|
+
},
|
|
271
|
+
abortController,
|
|
272
|
+
[Symbol.asyncIterator]: errorIterator,
|
|
273
|
+
[Symbol.dispose]: noop,
|
|
274
|
+
[Symbol.asyncDispose]: async () => { },
|
|
275
|
+
abortSilently: noop,
|
|
276
|
+
onIterate: () => noop,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (!response.body) {
|
|
280
|
+
throw new HttpException(HttpStatus.NULL, 'Stream body is falsy');
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
status: response.status,
|
|
284
|
+
abortController,
|
|
285
|
+
...readableStreamToAsyncIterable({
|
|
286
|
+
readableStream: response.body,
|
|
287
|
+
abortController,
|
|
288
|
+
}),
|
|
289
|
+
};
|
|
290
|
+
};
|
package/dist/client/fetcher.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { HttpException } from '../core/
|
|
2
|
-
import type {
|
|
1
|
+
import { HttpException } from '../core/http-exception.js';
|
|
2
|
+
import type { VovkFetcher, VovkFetcherOptions } from '../types/client.js';
|
|
3
3
|
import type { VovkHandlerSchema } from '../types/core.js';
|
|
4
4
|
export declare const DEFAULT_ERROR_MESSAGE = "Unknown error at default fetcher";
|
|
5
5
|
export type { VovkFetcher };
|
package/dist/client/fetcher.js
CHANGED
|
@@ -1,7 +1,48 @@
|
|
|
1
|
+
import { HttpException } from '../core/http-exception.js';
|
|
1
2
|
import { HttpStatus } from '../types/enums.js';
|
|
2
|
-
import {
|
|
3
|
-
import { fileNameToDisposition } from '../utils/fileNameToDisposition.js';
|
|
3
|
+
import { fileNameToDisposition } from '../utils/file-name-to-disposition.js';
|
|
4
4
|
export const DEFAULT_ERROR_MESSAGE = 'Unknown error at default fetcher';
|
|
5
|
+
// header values must be ByteString, escape non-ASCII as \uXXXX which JSON.parse reads natively
|
|
6
|
+
const toAsciiJson = (value) => JSON.stringify(value).replace(/[\u007f-\uffff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
7
|
+
// invokes emitError once for errors that surface during stream consumption, after the fetcher has returned
|
|
8
|
+
function wrapStreamErrors(stream, emitError) {
|
|
9
|
+
let emitted = false;
|
|
10
|
+
const emitOnce = async (error) => {
|
|
11
|
+
if (emitted)
|
|
12
|
+
return;
|
|
13
|
+
emitted = true;
|
|
14
|
+
await emitError(error);
|
|
15
|
+
};
|
|
16
|
+
const getIterator = stream[Symbol.asyncIterator].bind(stream);
|
|
17
|
+
const asPromise = stream.asPromise.bind(stream);
|
|
18
|
+
return Object.assign(stream, {
|
|
19
|
+
[Symbol.asyncIterator]: () => {
|
|
20
|
+
const iterator = getIterator();
|
|
21
|
+
return {
|
|
22
|
+
next: async () => {
|
|
23
|
+
try {
|
|
24
|
+
return await iterator.next();
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
await emitOnce(error);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
return: iterator.return?.bind(iterator),
|
|
32
|
+
throw: iterator.throw?.bind(iterator),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
asPromise: async () => {
|
|
36
|
+
try {
|
|
37
|
+
return await asPromise();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
await emitOnce(error);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
5
46
|
/**
|
|
6
47
|
* Creates a customizable fetcher function for client requests.
|
|
7
48
|
* @see https://vovk.dev/imports
|
|
@@ -62,7 +103,7 @@ export function createFetcher({ prepareRequestInit, transformResponse, onSuccess
|
|
|
62
103
|
accept: 'application/jsonl, application/json',
|
|
63
104
|
...(resolvedContentType ? { 'content-type': resolvedContentType } : {}),
|
|
64
105
|
...(resolvedFileName ? { 'content-disposition': fileNameToDisposition(resolvedFileName) } : {}),
|
|
65
|
-
...(meta ? { 'x-meta':
|
|
106
|
+
...(meta ? { 'x-meta': toAsciiJson(meta) } : {}),
|
|
66
107
|
};
|
|
67
108
|
// Normalize user headers to lowercase keys via Headers API (handles plain objects, arrays, and Headers instances)
|
|
68
109
|
const userHeaders = init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : {};
|
|
@@ -87,7 +128,10 @@ export function createFetcher({ prepareRequestInit, transformResponse, onSuccess
|
|
|
87
128
|
requestInit.body = JSON.stringify(body);
|
|
88
129
|
}
|
|
89
130
|
const abortController = new AbortController();
|
|
90
|
-
|
|
131
|
+
// keep the internal controller for stream disposal but let a user-provided init.signal abort too
|
|
132
|
+
requestInit.signal = init?.signal
|
|
133
|
+
? AbortSignal.any([abortController.signal, init.signal])
|
|
134
|
+
: abortController.signal;
|
|
91
135
|
requestInit = prepareRequestInit ? await prepareRequestInit(requestInit, inputOptions) : requestInit;
|
|
92
136
|
try {
|
|
93
137
|
response = await fetch(endpoint, requestInit);
|
|
@@ -103,7 +147,12 @@ export function createFetcher({ prepareRequestInit, transformResponse, onSuccess
|
|
|
103
147
|
}
|
|
104
148
|
const contentType = interpretAs ?? response.headers.get('content-type');
|
|
105
149
|
if (contentType?.startsWith('application/jsonl')) {
|
|
106
|
-
|
|
150
|
+
// route mid-stream errors to onError callbacks, which otherwise never see them
|
|
151
|
+
respData = wrapStreamErrors(defaultStreamHandler({ response, abortController }), async (error) => {
|
|
152
|
+
for (const cb of onErrorCallbacks) {
|
|
153
|
+
await cb(error, inputOptions, { response, init: requestInit, respData, schema });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
107
156
|
}
|
|
108
157
|
else if (contentType?.startsWith('application/json')) {
|
|
109
158
|
respData = await defaultHandler({ response, schema });
|