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.
- package/CHANGELOG.md +74 -0
- package/dist/client/create-rpc.d.ts +3 -3
- package/dist/client/create-rpc.js +6 -8
- package/dist/client/default-stream-handler.d.ts +1 -5
- package/dist/client/default-stream-handler.js +53 -45
- package/dist/client/fetcher.d.ts +1 -1
- package/dist/client/fetcher.js +53 -4
- package/dist/client/progressive.d.ts +1 -5
- package/dist/client/progressive.js +19 -9
- package/dist/client/serialize-query.d.ts +0 -11
- package/dist/client/serialize-query.js +4 -28
- package/dist/core/controllers-to-static-params.d.ts +1 -2
- package/dist/core/controllers-to-static-params.js +1 -2
- package/dist/core/create-decorator.d.ts +1 -1
- package/dist/core/decorate.d.ts +2 -6
- package/dist/core/decorators.js +3 -3
- package/dist/core/get-schema.d.ts +1 -1
- package/dist/core/get-schema.js +1 -3
- package/dist/core/http-exception.d.ts +1 -1
- package/dist/core/http-exception.js +1 -1
- package/dist/core/init-segment.js +1 -1
- package/dist/core/json-lines-responder.d.ts +8 -12
- package/dist/core/json-lines-responder.js +42 -20
- package/dist/core/vovk-app.d.ts +1 -1
- package/dist/core/vovk-app.js +62 -16
- package/dist/index.d.ts +16 -18
- package/dist/index.js +14 -15
- package/dist/internal.d.ts +14 -12
- package/dist/internal.js +8 -7
- package/dist/openapi/error.js +2 -2
- package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.d.ts +1 -12
- package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.js +4 -10
- package/dist/openapi/openapi-to-vovk-schema/index.d.ts +1 -1
- package/dist/openapi/openapi-to-vovk-schema/index.js +20 -6
- package/dist/openapi/openapi-to-vovk-schema/inline-refs.d.ts +0 -7
- package/dist/openapi/openapi-to-vovk-schema/inline-refs.js +2 -13
- package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.d.ts +0 -5
- package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.js +3 -8
- package/dist/openapi/vovk-schema-to-openapi.d.ts +1 -1
- package/dist/openapi/vovk-schema-to-openapi.js +9 -3
- package/dist/req/parse-query.d.ts +0 -23
- package/dist/req/parse-query.js +51 -43
- package/dist/req/validate-content-type.js +1 -1
- package/dist/samples/create-code-samples.d.ts +1 -1
- package/dist/samples/create-code-samples.js +4 -3
- package/dist/samples/schema-to-code.d.ts +1 -1
- package/dist/samples/schema-to-code.js +18 -15
- package/dist/samples/schema-to-object.d.ts +1 -1
- package/dist/samples/schema-to-object.js +18 -15
- package/dist/tools/create-tool-factory.d.ts +2 -2
- package/dist/tools/derive-tools.d.ts +9 -13
- package/dist/tools/derive-tools.js +48 -47
- package/dist/tools/to-model-output-default.d.ts +2 -2
- package/dist/tools/to-model-output-default.js +20 -6
- package/dist/tools/to-model-output-mcp.d.ts +3 -2
- package/dist/tools/to-model-output.d.ts +1 -1
- package/dist/types/client.d.ts +4 -4
- 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 +2 -3
- 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 +0 -5
- package/dist/utils/camel-case.js +2 -10
- package/dist/utils/deep-extend.d.ts +1 -10
- package/dist/utils/deep-extend.js +16 -4
- package/dist/validation/create-standard-validation.d.ts +3 -126
- package/dist/validation/create-standard-validation.js +2 -3
- package/dist/validation/create-validate-on-client.d.ts +3 -3
- 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/validation-schemas-object-to-single-validation-schema.d.ts +0 -9
- package/dist/validation/validation-schemas-object-to-single-validation-schema.js +11 -14
- package/dist/validation/with-validation-library.d.ts +2 -2
- package/dist/validation/with-validation-library.js +11 -5
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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 ---
|
package/dist/client/fetcher.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpException } from '../core/http-exception.js';
|
|
2
|
-
import type {
|
|
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 { 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':
|
|
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 });
|
|
@@ -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(
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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>;
|
package/dist/core/decorate.d.ts
CHANGED
|
@@ -9,12 +9,8 @@ export type DecorateMetadata = {
|
|
|
9
9
|
decoratorAppliers?: ((controller: KnownAny, propertyKey: string) => void)[];
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
|
-
* Applies decorators
|
|
13
|
-
*
|
|
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(
|
package/dist/core/decorators.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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 {
|
|
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;
|
package/dist/core/get-schema.js
CHANGED
|
@@ -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
|
-
*
|
|
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,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;
|