vovk 3.5.0 → 3.7.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/dist/client/create-rpc.d.ts +13 -0
- package/dist/client/create-rpc.js +147 -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 +16 -0
- package/dist/client/default-stream-handler.js +282 -0
- package/dist/client/fetcher.d.ts +1 -1
- package/dist/client/fetcher.js +2 -2
- package/dist/client/serialize-query.d.ts +13 -0
- package/dist/client/serialize-query.js +62 -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 +13 -0
- package/dist/core/controllers-to-static-params.js +32 -0
- package/dist/core/create-decorator.d.ts +12 -0
- package/dist/core/create-decorator.js +52 -0
- package/dist/core/decorators.js +4 -4
- package/dist/core/get-schema.d.ts +21 -0
- package/dist/core/get-schema.js +31 -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 +42 -0
- package/dist/core/json-lines-responder.js +94 -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 +318 -0
- package/dist/index.d.ts +10 -10
- package/dist/index.js +10 -10
- package/dist/internal.d.ts +10 -10
- package/dist/internal.js +9 -9
- package/dist/openapi/error.js +1 -1
- package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.d.ts +23 -0
- package/dist/openapi/openapi-to-vovk-schema/apply-components-schemas.js +90 -0
- package/dist/openapi/openapi-to-vovk-schema/index.d.ts +5 -0
- package/dist/openapi/openapi-to-vovk-schema/index.js +179 -0
- package/dist/openapi/openapi-to-vovk-schema/inline-refs.d.ts +9 -0
- package/dist/openapi/openapi-to-vovk-schema/inline-refs.js +99 -0
- package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.d.ts +7 -0
- package/dist/openapi/openapi-to-vovk-schema/prune-components-schemas.js +51 -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 +250 -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 +24 -0
- package/dist/req/parse-query.js +156 -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 +293 -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 +264 -0
- package/dist/samples/schema-to-object.d.ts +2 -0
- package/dist/samples/schema-to-object.js +164 -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 +46 -0
- package/dist/tools/derive-tools.js +131 -0
- package/dist/tools/to-model-output-default.d.ts +7 -0
- package/dist/tools/to-model-output-default.js +7 -0
- package/dist/tools/to-model-output-mcp.d.ts +30 -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 +3 -3
- package/dist/types/core.d.ts +1 -1
- package/dist/types/inference.d.ts +1 -1
- package/dist/types/validation.d.ts +1 -1
- package/dist/utils/camel-case.d.ts +6 -0
- package/dist/utils/camel-case.js +34 -0
- package/dist/utils/deep-extend.d.ts +54 -0
- package/dist/utils/deep-extend.js +127 -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 +268 -0
- package/dist/validation/create-standard-validation.js +45 -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/procedure.d.ts +24 -24
- package/dist/validation/procedure.js +1 -1
- package/dist/validation/validation-schemas-object-to-single-validation-schema.d.ts +17 -0
- package/dist/validation/validation-schemas-object-to-single-validation-schema.js +92 -0
- package/dist/validation/with-validation-library.d.ts +119 -0
- package/dist/validation/with-validation-library.js +184 -0
- package/package.json +13 -5
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { VovkHandlerSchema } from '../internal.js';
|
|
2
|
+
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
|
+
import type { KnownAny } from '../types/utils.js';
|
|
6
|
+
export type { VovkHandlerSchema, VovkRequest, CombinedSpec };
|
|
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,147 @@
|
|
|
1
|
+
import { fetcher as defaultFetcher } from './fetcher.js';
|
|
2
|
+
import { defaultHandler } from './default-handler.js';
|
|
3
|
+
import { defaultStreamHandler } from './default-stream-handler.js';
|
|
4
|
+
import { serializeQuery } from './serialize-query.js';
|
|
5
|
+
import { deepExtend } from '../utils/deep-extend.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
|
+
result = result.replace(`{${key}}`, value);
|
|
12
|
+
}
|
|
13
|
+
return `${result}${queryStr ? `?${queryStr}` : ''}`;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Creates a client-side RPC module for interacting with server-side controllers.
|
|
17
|
+
* @see https://vovk.dev/typescript
|
|
18
|
+
*/
|
|
19
|
+
export const createRPC = (givenSchema, segmentName, rpcModuleName, givenFetcher, options) => {
|
|
20
|
+
const schema = givenSchema; // fixes incompatibilities with JSON module
|
|
21
|
+
// fetcher ??= defaultFetcher as NonNullable<typeof fetcher>;
|
|
22
|
+
const segmentNamePath = options?.segmentNameOverride ?? segmentName;
|
|
23
|
+
const segmentSchema = schema.segments[segmentName];
|
|
24
|
+
if (!segmentSchema)
|
|
25
|
+
throw new Error(`Unable to create RPC module. Segment schema is missing for segment "${segmentName}".`);
|
|
26
|
+
let controllerSchema = schema.segments[segmentName]?.controllers[rpcModuleName];
|
|
27
|
+
const client = {};
|
|
28
|
+
if (!controllerSchema) {
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
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
|
+
return createRPC(schema, segmentName, rpcModuleName, givenFetcher, {
|
|
138
|
+
...options,
|
|
139
|
+
...newOptions,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
enumerable: false,
|
|
143
|
+
writable: false,
|
|
144
|
+
configurable: false,
|
|
145
|
+
});
|
|
146
|
+
return client;
|
|
147
|
+
};
|
|
@@ -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,16 @@
|
|
|
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
|
+
/**
|
|
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
|
+
*/
|
|
9
|
+
export declare const readableStreamToAsyncIterable: <T = unknown>({ readableStream, abortController, }: {
|
|
10
|
+
readableStream: ReadableStream<Uint8Array | string>;
|
|
11
|
+
abortController?: AbortController;
|
|
12
|
+
}) => Omit<VovkStreamAsyncIterable<T>, "abortController" | "status">;
|
|
13
|
+
export declare const defaultStreamHandler: ({ response, abortController, }: {
|
|
14
|
+
response: Response;
|
|
15
|
+
abortController: AbortController;
|
|
16
|
+
}) => VovkStreamAsyncIterable<unknown>;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { HttpStatus } from '../types/enums.js';
|
|
2
|
+
import { HttpException } from '../core/http-exception.js';
|
|
3
|
+
import '../utils/shim.js';
|
|
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
|
+
*/
|
|
10
|
+
export const readableStreamToAsyncIterable = ({ readableStream, abortController, }) => {
|
|
11
|
+
const reader = readableStream.getReader();
|
|
12
|
+
const subscribers = new Set();
|
|
13
|
+
// State
|
|
14
|
+
let isAbortedWithoutError = false;
|
|
15
|
+
let streamExhausted = false;
|
|
16
|
+
let streamError = null;
|
|
17
|
+
let errorIndex = -1;
|
|
18
|
+
let primaryStarted = false;
|
|
19
|
+
const cachedItems = [];
|
|
20
|
+
const waiters = [];
|
|
21
|
+
// --- Helper functions ---
|
|
22
|
+
const notifyWaiters = () => {
|
|
23
|
+
for (let i = waiters.length - 1; i >= 0; i--) {
|
|
24
|
+
const waiter = waiters[i];
|
|
25
|
+
let handled = false;
|
|
26
|
+
if (streamError && waiter.index >= errorIndex) {
|
|
27
|
+
waiter.reject(streamError);
|
|
28
|
+
handled = true;
|
|
29
|
+
}
|
|
30
|
+
else if (waiter.index < cachedItems.length) {
|
|
31
|
+
waiter.resolve({ value: cachedItems[waiter.index], done: false });
|
|
32
|
+
handled = true;
|
|
33
|
+
}
|
|
34
|
+
else if (streamExhausted || (abortController?.signal.aborted && isAbortedWithoutError)) {
|
|
35
|
+
waiter.resolve({ value: undefined, done: true });
|
|
36
|
+
handled = true;
|
|
37
|
+
}
|
|
38
|
+
if (handled) {
|
|
39
|
+
waiters.splice(i, 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const setStreamError = (error) => {
|
|
44
|
+
errorIndex = cachedItems.length;
|
|
45
|
+
streamError = error;
|
|
46
|
+
notifyWaiters();
|
|
47
|
+
};
|
|
48
|
+
const disposeStream = (reason) => {
|
|
49
|
+
isAbortedWithoutError = true;
|
|
50
|
+
streamExhausted = true;
|
|
51
|
+
notifyWaiters();
|
|
52
|
+
abortController?.abort(reason);
|
|
53
|
+
reader.cancel().catch(() => { });
|
|
54
|
+
};
|
|
55
|
+
// --- Primary reader ---
|
|
56
|
+
const runPrimaryReader = async () => {
|
|
57
|
+
let buffer = '';
|
|
58
|
+
let iterationIndex = 0;
|
|
59
|
+
// Returns true if the stream should stop (error encountered)
|
|
60
|
+
const processLine = (line) => {
|
|
61
|
+
let data;
|
|
62
|
+
try {
|
|
63
|
+
data = JSON.parse(line);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (data) {
|
|
69
|
+
subscribers.forEach((cb) => {
|
|
70
|
+
if (!abortController?.signal.aborted)
|
|
71
|
+
cb(data, iterationIndex);
|
|
72
|
+
});
|
|
73
|
+
iterationIndex++;
|
|
74
|
+
if (typeof data === 'object' && data !== null && 'isError' in data && 'reason' in data) {
|
|
75
|
+
const upcomingError = data.reason;
|
|
76
|
+
abortController?.abort(upcomingError);
|
|
77
|
+
const error = typeof upcomingError === 'string' ? new Error(upcomingError) : upcomingError;
|
|
78
|
+
setStreamError(error);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
else if (!abortController?.signal.aborted) {
|
|
82
|
+
cachedItems.push(data);
|
|
83
|
+
notifyWaiters();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
while (true) {
|
|
90
|
+
if (abortController?.signal.aborted && isAbortedWithoutError) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
let value;
|
|
94
|
+
let done;
|
|
95
|
+
try {
|
|
96
|
+
({ value, done } = await reader.read());
|
|
97
|
+
if (done)
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error?.name === 'AbortError' && isAbortedWithoutError) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
const err = new Error(`JSONLines stream error. ${String(error)}`);
|
|
105
|
+
err.cause = error;
|
|
106
|
+
setStreamError(err);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const chunk = typeof value === 'string'
|
|
110
|
+
? value
|
|
111
|
+
: typeof value === 'number'
|
|
112
|
+
? String.fromCharCode(value)
|
|
113
|
+
: new TextDecoder().decode(value);
|
|
114
|
+
buffer += chunk;
|
|
115
|
+
let newlineIdx;
|
|
116
|
+
while (true) {
|
|
117
|
+
newlineIdx = buffer.indexOf('\n');
|
|
118
|
+
if (newlineIdx === -1)
|
|
119
|
+
break;
|
|
120
|
+
if (abortController?.signal.aborted && isAbortedWithoutError) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
const line = buffer.slice(0, newlineIdx);
|
|
124
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
125
|
+
if (!line)
|
|
126
|
+
continue;
|
|
127
|
+
if (processLine(line))
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (abortController?.signal.aborted && isAbortedWithoutError) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Process any remaining data in the buffer (last line without trailing newline)
|
|
135
|
+
const remaining = buffer.trim();
|
|
136
|
+
if (remaining) {
|
|
137
|
+
processLine(remaining);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
streamExhausted = true;
|
|
142
|
+
notifyWaiters();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
// --- Async iterator ---
|
|
146
|
+
async function* asyncIterator() {
|
|
147
|
+
if (!primaryStarted) {
|
|
148
|
+
primaryStarted = true;
|
|
149
|
+
void runPrimaryReader();
|
|
150
|
+
}
|
|
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
|
|
173
|
+
if (streamError && index >= errorIndex) {
|
|
174
|
+
reject(streamError);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (abortController?.signal.aborted && isAbortedWithoutError) {
|
|
178
|
+
resolve({ value: undefined, done: true });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (index < cachedItems.length) {
|
|
182
|
+
resolve({ value: cachedItems[index], done: false });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (streamExhausted) {
|
|
186
|
+
resolve({ value: undefined, done: true });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
waiters.push({ index, resolve, reject });
|
|
190
|
+
});
|
|
191
|
+
if (result.done) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
index++;
|
|
195
|
+
yield result.value;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// --- Public API ---
|
|
199
|
+
const asPromise = async () => {
|
|
200
|
+
const items = [];
|
|
201
|
+
for await (const item of asyncIterator()) {
|
|
202
|
+
items.push(item);
|
|
203
|
+
}
|
|
204
|
+
return items;
|
|
205
|
+
};
|
|
206
|
+
const abortSilently = (reason) => {
|
|
207
|
+
isAbortedWithoutError = true;
|
|
208
|
+
streamExhausted = true;
|
|
209
|
+
notifyWaiters();
|
|
210
|
+
abortController?.abort(reason);
|
|
211
|
+
reader.cancel().catch(() => { });
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
asPromise,
|
|
215
|
+
// abortController,
|
|
216
|
+
[Symbol.asyncIterator]: asyncIterator,
|
|
217
|
+
[Symbol.dispose]: () => disposeStream('Stream disposed'),
|
|
218
|
+
[Symbol.asyncDispose]: async () => disposeStream('Stream async disposed'),
|
|
219
|
+
abortSilently,
|
|
220
|
+
onIterate: (cb) => {
|
|
221
|
+
if (abortController?.signal.aborted)
|
|
222
|
+
return () => { };
|
|
223
|
+
subscribers.add(cb);
|
|
224
|
+
return () => subscribers.delete(cb);
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
export const defaultStreamHandler = ({ response, abortController, }) => {
|
|
229
|
+
// Handle error responses by creating a stream that fails on first iteration
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
let cachedError = null;
|
|
232
|
+
let errorParsed = false;
|
|
233
|
+
// Parse error asynchronously and cache it
|
|
234
|
+
void response
|
|
235
|
+
.json()
|
|
236
|
+
.then((res) => {
|
|
237
|
+
cachedError = new HttpException(response.status, res.message ?? DEFAULT_ERROR_MESSAGE);
|
|
238
|
+
})
|
|
239
|
+
.catch((e) => {
|
|
240
|
+
cachedError = new HttpException(response.status, e.message ?? DEFAULT_ERROR_MESSAGE, e);
|
|
241
|
+
})
|
|
242
|
+
.finally(() => {
|
|
243
|
+
errorParsed = true;
|
|
244
|
+
});
|
|
245
|
+
const getError = async () => {
|
|
246
|
+
// Wait for error to be parsed
|
|
247
|
+
while (!errorParsed) {
|
|
248
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
249
|
+
}
|
|
250
|
+
return cachedError ?? new HttpException(response.status, DEFAULT_ERROR_MESSAGE);
|
|
251
|
+
};
|
|
252
|
+
const errorIterator = () => ({
|
|
253
|
+
async next() {
|
|
254
|
+
throw await getError();
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
const noop = () => { };
|
|
258
|
+
return {
|
|
259
|
+
status: response.status,
|
|
260
|
+
asPromise: async () => {
|
|
261
|
+
throw await getError();
|
|
262
|
+
},
|
|
263
|
+
abortController,
|
|
264
|
+
[Symbol.asyncIterator]: errorIterator,
|
|
265
|
+
[Symbol.dispose]: noop,
|
|
266
|
+
[Symbol.asyncDispose]: async () => { },
|
|
267
|
+
abortSilently: noop,
|
|
268
|
+
onIterate: () => noop,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (!response.body) {
|
|
272
|
+
throw new HttpException(HttpStatus.NULL, 'Stream body is falsy');
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
status: response.status,
|
|
276
|
+
abortController,
|
|
277
|
+
...readableStreamToAsyncIterable({
|
|
278
|
+
readableStream: response.body,
|
|
279
|
+
abortController,
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
};
|
package/dist/client/fetcher.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HttpException } from '../core/
|
|
1
|
+
import { HttpException } from '../core/http-exception.js';
|
|
2
2
|
import type { VovkFetcherOptions, VovkFetcher } 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";
|
package/dist/client/fetcher.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpStatus } from '../types/enums.js';
|
|
2
|
-
import { HttpException } from '../core/
|
|
3
|
-
import { fileNameToDisposition } from '../utils/
|
|
2
|
+
import { HttpException } from '../core/http-exception.js';
|
|
3
|
+
import { fileNameToDisposition } from '../utils/file-name-to-disposition.js';
|
|
4
4
|
export const DEFAULT_ERROR_MESSAGE = 'Unknown error at default fetcher';
|
|
5
5
|
/**
|
|
6
6
|
* Creates a customizable fetcher function for client requests.
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
export declare function serializeQuery(obj: Record<string, KnownAny>): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
*/
|
|
8
|
+
function buildParams(key, value) {
|
|
9
|
+
if (value === null || value === undefined) {
|
|
10
|
+
return []; // skip null/undefined values entirely
|
|
11
|
+
}
|
|
12
|
+
// If value is an object or array, we need to recurse
|
|
13
|
+
if (typeof value === 'object') {
|
|
14
|
+
// Array case
|
|
15
|
+
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
|
+
*/
|
|
26
|
+
return value.flatMap((v, i) => {
|
|
27
|
+
const newKey = `${key}[${i}]`;
|
|
28
|
+
return buildParams(newKey, v);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Plain object case
|
|
32
|
+
return Object.keys(value).flatMap((k) => {
|
|
33
|
+
const newKey = `${key}[${k}]`;
|
|
34
|
+
return buildParams(newKey, value[k]);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return [`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`];
|
|
38
|
+
}
|
|
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
|
+
*/
|
|
50
|
+
export function serializeQuery(obj) {
|
|
51
|
+
if (!obj || typeof obj !== 'object')
|
|
52
|
+
return '';
|
|
53
|
+
// Collect query segments
|
|
54
|
+
const segments = [];
|
|
55
|
+
for (const key in obj) {
|
|
56
|
+
if (Object.hasOwn(obj, key)) {
|
|
57
|
+
const value = obj[key];
|
|
58
|
+
segments.push(...buildParams(key, value));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return segments.join('&');
|
|
62
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { VovkController } from '../types/core.js';
|
|
2
|
+
import type { KnownAny } from '../types/utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Adapts a decorator callback to work with experimental, 2018-09, and TC39 Stage 3 decorator formats.
|
|
5
|
+
* The callback receives (controller, propertyKey) when the class and field/method value are available.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyDecoratorAdapter(arg1: unknown, arg2: unknown, callback: (controller: VovkController, propertyKey: string) => void): KnownAny;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapts a decorator callback to work with experimental, 2018-09, and TC39 Stage 3 decorator formats.
|
|
3
|
+
* The callback receives (controller, propertyKey) when the class and field/method value are available.
|
|
4
|
+
*/
|
|
5
|
+
export function applyDecoratorAdapter(arg1, arg2, callback) {
|
|
6
|
+
// Experimental decorators: (target, propertyKey: string)
|
|
7
|
+
if (typeof arg2 === 'string') {
|
|
8
|
+
callback(arg1, arg2);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
// TC39 Stage 3: (value, context: { kind, name, addInitializer })
|
|
12
|
+
if (typeof arg2 === 'object' && arg2 !== null && 'name' in arg2) {
|
|
13
|
+
const ctx = arg2;
|
|
14
|
+
const propertyKey = String(ctx.name);
|
|
15
|
+
if (ctx.kind === 'field') {
|
|
16
|
+
return function (initialValue) {
|
|
17
|
+
this[propertyKey] = initialValue;
|
|
18
|
+
callback(this, propertyKey);
|
|
19
|
+
return this[propertyKey];
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
ctx.addInitializer(function () {
|
|
23
|
+
callback(this, propertyKey);
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// 2018-09 proposal: (descriptor: { kind, key, placement, initializer? })
|
|
28
|
+
if (typeof arg1 === 'object' && arg1 !== null && 'kind' in arg1 && 'key' in arg1) {
|
|
29
|
+
const desc = arg1;
|
|
30
|
+
const propertyKey = String(desc.key);
|
|
31
|
+
if (desc.kind === 'field') {
|
|
32
|
+
const origInit = desc.initializer;
|
|
33
|
+
return {
|
|
34
|
+
...desc,
|
|
35
|
+
initializer() {
|
|
36
|
+
const value = origInit?.call(this);
|
|
37
|
+
this[propertyKey] = value;
|
|
38
|
+
callback(this, propertyKey);
|
|
39
|
+
return this[propertyKey];
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
...desc,
|
|
45
|
+
finisher(klass) {
|
|
46
|
+
callback(klass, propertyKey);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StaticClass } from '../types/utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generates static API of the given controllers for a static segment.
|
|
4
|
+
* @see https://vovk.dev/segment
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* export function generateStaticParams() {
|
|
8
|
+
* return controllersToStaticParams(controllers);
|
|
9
|
+
* }
|
|
10
|
+
*/
|
|
11
|
+
export declare function controllersToStaticParams(c: Record<string, StaticClass>, slug?: string): {
|
|
12
|
+
[slug]: string[];
|
|
13
|
+
}[];
|