vovk 3.7.0 → 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.
Files changed (82) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/client/create-rpc.d.ts +3 -3
  3. package/dist/client/create-rpc.js +6 -8
  4. package/dist/client/default-stream-handler.d.ts +1 -5
  5. package/dist/client/default-stream-handler.js +53 -45
  6. package/dist/client/fetcher.d.ts +1 -1
  7. package/dist/client/fetcher.js +53 -4
  8. package/dist/client/progressive.d.ts +1 -5
  9. package/dist/client/progressive.js +19 -9
  10. package/dist/client/serialize-query.d.ts +0 -11
  11. package/dist/client/serialize-query.js +4 -28
  12. package/dist/core/controllers-to-static-params.d.ts +1 -2
  13. package/dist/core/controllers-to-static-params.js +1 -2
  14. package/dist/core/create-decorator.d.ts +1 -1
  15. package/dist/core/decorate.d.ts +2 -6
  16. package/dist/core/decorators.js +3 -3
  17. package/dist/core/get-schema.d.ts +1 -1
  18. package/dist/core/get-schema.js +1 -3
  19. package/dist/core/http-exception.d.ts +1 -1
  20. package/dist/core/http-exception.js +1 -1
  21. package/dist/core/init-segment.js +1 -1
  22. package/dist/core/json-lines-responder.d.ts +8 -12
  23. package/dist/core/json-lines-responder.js +42 -20
  24. package/dist/core/vovk-app.d.ts +1 -1
  25. package/dist/core/vovk-app.js +62 -16
  26. package/dist/index.d.ts +16 -18
  27. package/dist/index.js +14 -15
  28. package/dist/internal.d.ts +14 -12
  29. package/dist/internal.js +8 -7
  30. package/dist/openapi/error.js +2 -2
  31. package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.d.ts +1 -12
  32. package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.js +4 -10
  33. package/dist/openapi/openapi-to-vovk-schema/index.d.ts +1 -1
  34. package/dist/openapi/openapi-to-vovk-schema/index.js +20 -6
  35. package/dist/openapi/openapi-to-vovk-schema/inline-refs.d.ts +0 -7
  36. package/dist/openapi/openapi-to-vovk-schema/inline-refs.js +2 -13
  37. package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.d.ts +0 -5
  38. package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.js +3 -8
  39. package/dist/openapi/vovk-schema-to-openapi.d.ts +1 -1
  40. package/dist/openapi/vovk-schema-to-openapi.js +9 -3
  41. package/dist/req/parse-query.d.ts +0 -23
  42. package/dist/req/parse-query.js +51 -43
  43. package/dist/req/validate-content-type.js +1 -1
  44. package/dist/samples/create-code-samples.d.ts +1 -1
  45. package/dist/samples/create-code-samples.js +4 -3
  46. package/dist/samples/schema-to-code.d.ts +1 -1
  47. package/dist/samples/schema-to-code.js +18 -15
  48. package/dist/samples/schema-to-object.d.ts +1 -1
  49. package/dist/samples/schema-to-object.js +18 -15
  50. package/dist/tools/create-tool-factory.d.ts +2 -2
  51. package/dist/tools/derive-tools.d.ts +9 -13
  52. package/dist/tools/derive-tools.js +48 -47
  53. package/dist/tools/to-model-output-default.d.ts +2 -2
  54. package/dist/tools/to-model-output-default.js +20 -6
  55. package/dist/tools/to-model-output-mcp.d.ts +3 -2
  56. package/dist/tools/to-model-output.d.ts +1 -1
  57. package/dist/types/client.d.ts +4 -4
  58. package/dist/types/config.d.ts +5 -6
  59. package/dist/types/core.d.ts +4 -1
  60. package/dist/types/enums.d.ts +1 -1
  61. package/dist/types/enums.js +1 -1
  62. package/dist/types/inference.d.ts +2 -3
  63. package/dist/types/request.d.ts +1 -5
  64. package/dist/types/standard-schema.js +0 -2
  65. package/dist/types/standard-tool.d.ts +8 -12
  66. package/dist/types/tools.d.ts +2 -40
  67. package/dist/types/validation.d.ts +3 -3
  68. package/dist/utils/camel-case.d.ts +0 -5
  69. package/dist/utils/camel-case.js +2 -10
  70. package/dist/utils/deep-extend.d.ts +1 -10
  71. package/dist/utils/deep-extend.js +16 -4
  72. package/dist/validation/create-standard-validation.d.ts +3 -126
  73. package/dist/validation/create-standard-validation.js +2 -3
  74. package/dist/validation/create-validate-on-client.d.ts +3 -3
  75. package/dist/validation/json-schema-only-spec.d.ts +14 -0
  76. package/dist/validation/json-schema-only-spec.js +57 -0
  77. package/dist/validation/procedure.d.ts +1 -124
  78. package/dist/validation/validation-schemas-object-to-single-validation-schema.d.ts +0 -9
  79. package/dist/validation/validation-schemas-object-to-single-validation-schema.js +11 -14
  80. package/dist/validation/with-validation-library.d.ts +2 -2
  81. package/dist/validation/with-validation-library.js +11 -5
  82. package/package.json +3 -10
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.
@@ -1,9 +1,9 @@
1
1
  import type { VovkHandlerSchema } from '../internal.js';
2
+ import type { VovkFetcher, VovkFetcherOptions, VovkRPCModule } from '../types/client.js';
2
3
  import type { VovkRequest } from '../types/request.js';
3
- import type { VovkRPCModule, VovkFetcher, VovkFetcherOptions } from '../types/client.js';
4
- import type { CombinedSpec } from '../types/validation.js';
5
4
  import type { KnownAny } from '../types/utils.js';
6
- export type { VovkHandlerSchema, VovkRequest, CombinedSpec };
5
+ import type { CombinedSpec } from '../types/validation.js';
6
+ export type { CombinedSpec, VovkHandlerSchema, VovkRequest };
7
7
  /**
8
8
  * Creates a client-side RPC module for interacting with server-side controllers.
9
9
  * @see https://vovk.dev/typescript
@@ -1,14 +1,15 @@
1
- import { fetcher as defaultFetcher } from './fetcher.js';
1
+ import { deepExtend } from '../utils/deep-extend.js';
2
2
  import { defaultHandler } from './default-handler.js';
3
3
  import { defaultStreamHandler } from './default-stream-handler.js';
4
+ import { fetcher as defaultFetcher } from './fetcher.js';
4
5
  import { serializeQuery } from './serialize-query.js';
5
- import { deepExtend } from '../utils/deep-extend.js';
6
6
  const trimPath = (path) => path.trim().replace(/^\/|\/$/g, '');
7
7
  const getHandlerPath = (endpoint, params, query) => {
8
8
  let result = endpoint;
9
9
  const queryStr = query ? serializeQuery(query) : null;
10
10
  for (const [key, value] of Object.entries(params ?? {})) {
11
- result = result.replace(`{${key}}`, value);
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)));
12
13
  }
13
14
  return `${result}${queryStr ? `?${queryStr}` : ''}`;
14
15
  };
@@ -26,7 +27,6 @@ export const createRPC = (givenSchema, segmentName, rpcModuleName, givenFetcher,
26
27
  let controllerSchema = schema.segments[segmentName]?.controllers[rpcModuleName];
27
28
  const client = {};
28
29
  if (!controllerSchema) {
29
- // eslint-disable-next-line no-console
30
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
31
  controllerSchema = {
32
32
  rpcModuleName,
@@ -134,10 +134,8 @@ export const createRPC = (givenSchema, segmentName, rpcModuleName, givenFetcher,
134
134
  }
135
135
  Object.defineProperty(client, 'withDefaults', {
136
136
  value: (newOptions) => {
137
- return createRPC(schema, segmentName, rpcModuleName, givenFetcher, {
138
- ...options,
139
- ...newOptions,
140
- });
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));
141
139
  },
142
140
  enumerable: false,
143
141
  writable: false,
@@ -1,11 +1,7 @@
1
1
  import type { VovkStreamAsyncIterable } from '../types/client.js';
2
2
  import '../utils/shim.js';
3
3
  export declare const DEFAULT_ERROR_MESSAGE = "An unknown error at the default stream handler";
4
- /**
5
- * Converts a ReadableStream of JSON Lines into a VovkStreamAsyncIterable.
6
- * This is the core streaming logic extracted for reuse outside of HTTP contexts.
7
- * @see https://vovk.dev/jsonlines
8
- */
4
+ /** ReadableStream of JSON Lines to VovkStreamAsyncIterable, reusable outside HTTP contexts. @see https://vovk.dev/jsonlines */
9
5
  export declare const readableStreamToAsyncIterable: <T = unknown>({ readableStream, abortController, }: {
10
6
  readableStream: ReadableStream<Uint8Array | string>;
11
7
  abortController?: AbortController;
@@ -1,12 +1,8 @@
1
- import { HttpStatus } from '../types/enums.js';
2
1
  import { HttpException } from '../core/http-exception.js';
2
+ import { HttpStatus } from '../types/enums.js';
3
3
  import '../utils/shim.js';
4
4
  export const DEFAULT_ERROR_MESSAGE = 'An unknown error at the default stream handler';
5
- /**
6
- * Converts a ReadableStream of JSON Lines into a VovkStreamAsyncIterable.
7
- * This is the core streaming logic extracted for reuse outside of HTTP contexts.
8
- * @see https://vovk.dev/jsonlines
9
- */
5
+ /** ReadableStream of JSON Lines to VovkStreamAsyncIterable, reusable outside HTTP contexts. @see https://vovk.dev/jsonlines */
10
6
  export const readableStreamToAsyncIterable = ({ readableStream, abortController, }) => {
11
7
  const reader = readableStream.getReader();
12
8
  const subscribers = new Set();
@@ -16,6 +12,8 @@ export const readableStreamToAsyncIterable = ({ readableStream, abortController,
16
12
  let streamError = null;
17
13
  let errorIndex = -1;
18
14
  let primaryStarted = false;
15
+ // consumers currently iterating, the last one to leave early releases the connection
16
+ let activeIterators = 0;
19
17
  const cachedItems = [];
20
18
  const waiters = [];
21
19
  // --- Helper functions ---
@@ -66,11 +64,7 @@ export const readableStreamToAsyncIterable = ({ readableStream, abortController,
66
64
  return false;
67
65
  }
68
66
  if (data) {
69
- subscribers.forEach((cb) => {
70
- if (!abortController?.signal.aborted)
71
- cb(data, iterationIndex);
72
- });
73
- iterationIndex++;
67
+ // the error envelope is a control message, not data, subscribers must not see it
74
68
  if (typeof data === 'object' && data !== null && 'isError' in data && 'reason' in data) {
75
69
  const upcomingError = data.reason;
76
70
  abortController?.abort(upcomingError);
@@ -78,7 +72,12 @@ export const readableStreamToAsyncIterable = ({ readableStream, abortController,
78
72
  setStreamError(error);
79
73
  return true;
80
74
  }
81
- else if (!abortController?.signal.aborted) {
75
+ subscribers.forEach((cb) => {
76
+ if (!abortController?.signal.aborted)
77
+ cb(data, iterationIndex);
78
+ });
79
+ iterationIndex++;
80
+ if (!abortController?.signal.aborted) {
82
81
  cachedItems.push(data);
83
82
  notifyWaiters();
84
83
  }
@@ -148,51 +147,60 @@ export const readableStreamToAsyncIterable = ({ readableStream, abortController,
148
147
  primaryStarted = true;
149
148
  void runPrimaryReader();
150
149
  }
150
+ activeIterators++;
151
151
  let index = 0;
152
- while (true) {
153
- // Check error first
154
- if (streamError && index >= errorIndex) {
155
- throw streamError;
156
- }
157
- // Clean exit on abort without error
158
- if (abortController?.signal.aborted && isAbortedWithoutError) {
159
- return;
160
- }
161
- // Yield from cache if available
162
- if (index < cachedItems.length) {
163
- yield cachedItems[index++];
164
- continue;
165
- }
166
- // Stream finished
167
- if (streamExhausted) {
168
- return;
169
- }
170
- // Wait for next item or completion
171
- const result = await new Promise((resolve, reject) => {
172
- // Re-check state inside promise to handle race conditions
152
+ try {
153
+ while (true) {
154
+ // Check error first
173
155
  if (streamError && index >= errorIndex) {
174
- reject(streamError);
175
- return;
156
+ throw streamError;
176
157
  }
158
+ // Clean exit on abort without error
177
159
  if (abortController?.signal.aborted && isAbortedWithoutError) {
178
- resolve({ value: undefined, done: true });
179
160
  return;
180
161
  }
162
+ // Yield from cache if available
181
163
  if (index < cachedItems.length) {
182
- resolve({ value: cachedItems[index], done: false });
183
- return;
164
+ yield cachedItems[index++];
165
+ continue;
184
166
  }
167
+ // Stream finished
185
168
  if (streamExhausted) {
186
- resolve({ value: undefined, done: true });
187
169
  return;
188
170
  }
189
- waiters.push({ index, resolve, reject });
190
- });
191
- if (result.done) {
192
- return;
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;
193
197
  }
194
- index++;
195
- yield result.value;
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');
196
204
  }
197
205
  }
198
206
  // --- Public API ---
@@ -1,5 +1,5 @@
1
1
  import { HttpException } from '../core/http-exception.js';
2
- import type { VovkFetcherOptions, VovkFetcher } from '../types/client.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 };
@@ -1,7 +1,48 @@
1
- import { HttpStatus } from '../types/enums.js';
2
1
  import { HttpException } from '../core/http-exception.js';
2
+ import { HttpStatus } from '../types/enums.js';
3
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': JSON.stringify(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
- requestInit.signal = abortController.signal;
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
- respData = defaultStreamHandler({ response, abortController });
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 });
@@ -6,10 +6,6 @@ type PromisifyProperties<T> = {
6
6
  [K in keyof T]: Promise<T[K]>;
7
7
  };
8
8
  type TransformUnionToPromises<T> = PromisifyProperties<UnionToIntersection<T>>;
9
- /**
10
- * Implements progressive fetching by returning a proxy object where each property is a promise
11
- * that resolves when the corresponding value is available from the stream.
12
- * @see https://vovk.dev/jsonlines
13
- */
9
+ /** Proxy object where each property is a promise resolving once the value arrives from the stream. @see https://vovk.dev/jsonlines */
14
10
  export declare function progressive<T extends (...args: KnownAny[]) => Promise<VovkStreamAsyncIterable<KnownAny>>>(fn: T, ...args: undefined extends Parameters<T>[0] ? [arg?: Parameters<T>[0]] : [arg: Parameters<T>[0]]): TransformUnionToPromises<VovkYieldType<T>>;
15
11
  export {};
@@ -1,11 +1,9 @@
1
- /**
2
- * Implements progressive fetching by returning a proxy object where each property is a promise
3
- * that resolves when the corresponding value is available from the stream.
4
- * @see https://vovk.dev/jsonlines
5
- */
1
+ /** Proxy object where each property is a promise resolving once the value arrives from the stream. @see https://vovk.dev/jsonlines */
6
2
  export function progressive(fn, ...args) {
7
3
  const [arg] = args;
8
4
  const reg = {};
5
+ let finalState = null;
6
+ const missingKeyError = (key) => new Error(`The connection was closed without sending a value for "${key}"`);
9
7
  void fn(arg)
10
8
  .then(async (result) => {
11
9
  for await (const item of result) {
@@ -23,15 +21,17 @@ export function progressive(fn, ...args) {
23
21
  }
24
22
  }
25
23
  }
24
+ finalState = { type: 'done' };
26
25
  Object.keys(reg).forEach((key) => {
27
26
  if (reg[key].isSettled)
28
27
  return;
29
28
  reg[key].isSettled = true;
30
- reg[key].reject(new Error(`The connection was closed without sending a value for "${key}"`));
29
+ reg[key].reject(missingKeyError(key));
31
30
  });
32
31
  return result;
33
32
  })
34
33
  .catch((error) => {
34
+ finalState = { type: 'error', error };
35
35
  Object.keys(reg).forEach((key) => {
36
36
  if (reg[key].isSettled)
37
37
  return;
@@ -45,12 +45,22 @@ export function progressive(fn, ...args) {
45
45
  if (prop in reg) {
46
46
  return reg[prop].promise;
47
47
  }
48
+ // symbols and a non-yielded then are inspection or await probes, not stream keys
49
+ if (typeof prop === 'symbol' || prop === 'then') {
50
+ return undefined;
51
+ }
48
52
  const { promise, resolve, reject } = Promise.withResolvers();
49
53
  reg[prop] = { resolve, reject, promise, isSettled: false };
54
+ // the stream already finished, settle immediately instead of hanging forever
55
+ if (finalState) {
56
+ reg[prop].isSettled = true;
57
+ promise.catch(() => { });
58
+ reject(finalState.type === 'error' ? finalState.error : missingKeyError(prop));
59
+ }
50
60
  return promise;
51
61
  },
52
- ownKeys: () => {
53
- throw new Error('Getting own keys is not possible as they are dynamically created');
54
- },
62
+ has: (_target, prop) => prop in reg,
63
+ ownKeys: () => Reflect.ownKeys(reg),
64
+ getOwnPropertyDescriptor: (_target, prop) => prop in reg ? { enumerable: true, configurable: true, value: reg[prop].promise } : undefined,
55
65
  });
56
66
  }
@@ -1,13 +1,2 @@
1
1
  import type { KnownAny } from '../types/utils.js';
2
- /**
3
- * Serialize a nested object (including arrays, arrays of objects, etc.)
4
- * into a bracket-based query string.
5
- *
6
- * @example
7
- * serializeQuery({ x: 'xx', y: [1, 2], z: { f: 'x' } })
8
- * => "x=xx&y[0]=1&y[1]=2&z[f]=x"
9
- *
10
- * @param obj - The input object to be serialized
11
- * @returns - A bracket-based query string (without leading "?")
12
- */
13
2
  export declare function serializeQuery(obj: Record<string, KnownAny>): string;
@@ -1,10 +1,4 @@
1
- /**
2
- * Recursively build query parameters from an object.
3
- *
4
- * @param key - The query key so far (e.g. 'user', 'user[0]', 'user[0][name]')
5
- * @param value - The current value to serialize
6
- * @returns - An array of `key=value` strings
7
- */
1
+ // recursively builds "key=value" strings, key grows like 'user', 'user[0]', 'user[0][name]'
8
2
  function buildParams(key, value) {
9
3
  if (value === null || value === undefined) {
10
4
  return []; // skip null/undefined values entirely
@@ -13,16 +7,7 @@ function buildParams(key, value) {
13
7
  if (typeof value === 'object') {
14
8
  // Array case
15
9
  if (Array.isArray(value)) {
16
- /**
17
- * We use index-based bracket notation here:
18
- * e.g. for value = ['aa', 'bb'] and key = 'foo'
19
- * => "foo[0]=aa&foo[1]=bb"
20
- *
21
- * If you prefer "foo[]=aa&foo[]=bb" style, replace:
22
- * `${key}[${i}]`
23
- * with:
24
- * `${key}[]`
25
- */
10
+ // index-based brackets: ['aa', 'bb'] + 'foo' -> "foo[0]=aa&foo[1]=bb"
26
11
  return value.flatMap((v, i) => {
27
12
  const newKey = `${key}[${i}]`;
28
13
  return buildParams(newKey, v);
@@ -36,17 +21,8 @@ function buildParams(key, value) {
36
21
  }
37
22
  return [`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`];
38
23
  }
39
- /**
40
- * Serialize a nested object (including arrays, arrays of objects, etc.)
41
- * into a bracket-based query string.
42
- *
43
- * @example
44
- * serializeQuery({ x: 'xx', y: [1, 2], z: { f: 'x' } })
45
- * => "x=xx&y[0]=1&y[1]=2&z[f]=x"
46
- *
47
- * @param obj - The input object to be serialized
48
- * @returns - A bracket-based query string (without leading "?")
49
- */
24
+ // nested object to a bracket query string (no leading "?"),
25
+ // e.g. { x: 'xx', y: [1, 2], z: { f: 'x' } } -> "x=xx&y[0]=1&y[1]=2&z[f]=x"
50
26
  export function serializeQuery(obj) {
51
27
  if (!obj || typeof obj !== 'object')
52
28
  return '';
@@ -1,7 +1,6 @@
1
1
  import type { StaticClass } from '../types/utils.js';
2
2
  /**
3
- * Generates static API of the given controllers for a static segment.
4
- * @see https://vovk.dev/segment
3
+ * Static params for the controllers of a static segment. @see https://vovk.dev/segment
5
4
  * @example
6
5
  * ```ts
7
6
  * export function generateStaticParams() {
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Generates static API of the given controllers for a static segment.
3
- * @see https://vovk.dev/segment
2
+ * Static params for the controllers of a static segment. @see https://vovk.dev/segment
4
3
  * @example
5
4
  * ```ts
6
5
  * export function generateStaticParams() {
@@ -1,4 +1,4 @@
1
- import type { VovkHandlerSchema, VovkController } from '../types/core.js';
1
+ import type { VovkController, VovkHandlerSchema } from '../types/core.js';
2
2
  import type { VovkRequest } from '../types/request.js';
3
3
  import type { KnownAny } from '../types/utils.js';
4
4
  type Next = () => Promise<unknown>;
@@ -9,12 +9,8 @@ export type DecorateMetadata = {
9
9
  decoratorAppliers?: ((controller: KnownAny, propertyKey: string) => void)[];
10
10
  };
11
11
  /**
12
- * Applies decorators to a handler without using decorator syntax.
13
- * Returns an object with `.handle()` to register the handler function.
14
- *
15
- * When the last argument is a procedure result (has `.handle`), its `.handle()` is proxied.
16
- * Otherwise, `.handle()` wraps a plain handler directly.
17
- *
12
+ * Applies decorators without decorator syntax; `.handle()` registers the handler
13
+ * (proxied when the last arg is a procedure result, wraps a plain handler otherwise).
18
14
  * @example With procedure
19
15
  * ```ts
20
16
  * static handleParams = decorate(
@@ -1,8 +1,8 @@
1
- import { vovkApp } from './vovk-app.js';
2
- import { trimPath } from '../utils/trim-path.js';
1
+ import { HttpMethod } from '../types/enums.js';
3
2
  import { toKebabCase } from '../utils/to-kebab-case.js';
3
+ import { trimPath } from '../utils/trim-path.js';
4
4
  import { applyDecoratorAdapter } from './apply-decorator-adapter.js';
5
- import { HttpMethod } from '../types/enums.js';
5
+ import { vovkApp } from './vovk-app.js';
6
6
  const isClass = (func) => typeof func === 'function' && /class/.test(func.toString());
7
7
  const assignSchema = ({ controller, propertyKey, path, options, httpMethod, }) => {
8
8
  if (typeof window !== 'undefined') {
@@ -1,4 +1,4 @@
1
- import type { VovkSegmentSchema, VovkController } from '../types/core.js';
1
+ import type { VovkController, VovkSegmentSchema } from '../types/core.js';
2
2
  import type { StaticClass } from '../types/utils.js';
3
3
  export declare function getControllerSchema(controller: VovkController, rpcModuleName: string, exposeValidation: boolean): Promise<{
4
4
  rpcModuleName: string;
@@ -2,9 +2,7 @@ import { VovkSchemaIdEnum } from '../types/enums.js';
2
2
  export async function getControllerSchema(controller, rpcModuleName, exposeValidation) {
3
3
  const handlers = exposeValidation
4
4
  ? (controller._handlers ?? {})
5
- : Object.fromEntries(
6
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
- Object.entries(controller._handlers ?? {}).map(([key, { validation: _v, ...value }]) => [key, value]));
5
+ : Object.fromEntries(Object.entries(controller._handlers ?? {}).map(([key, { validation: _v, ...value }]) => [key, value]));
8
6
  return {
9
7
  rpcModuleName,
10
8
  originalControllerName: controller.name,
@@ -1,7 +1,7 @@
1
1
  import type { VovkErrorResponse } from '../types/core.js';
2
2
  import type { HttpStatus } from '../types/enums.js';
3
3
  /**
4
- * Represents an HTTP exception with a status code and message.
4
+ * HTTP exception with a status code and message.
5
5
  * @example
6
6
  * ```ts
7
7
  * throw new HttpException(HttpStatus.BAD_REQUEST, 'Invalid request data');
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Represents an HTTP exception with a status code and message.
2
+ * HTTP exception with a status code and message.
3
3
  * @example
4
4
  * ```ts
5
5
  * throw new HttpException(HttpStatus.BAD_REQUEST, 'Invalid request data');
@@ -1,6 +1,6 @@
1
- import { vovkApp } from './vovk-app.js';
2
1
  import { trimPath } from '../utils/trim-path.js';
3
2
  import { getSchema } from './get-schema.js';
3
+ import { vovkApp } from './vovk-app.js';
4
4
  export const initSegment = (options) => {
5
5
  const segmentName = trimPath(options.segmentName ?? '');
6
6
  options.segmentName = segmentName;