vovk 3.0.0-draft.7 → 3.0.0-draft.70

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 (101) hide show
  1. package/README.md +8 -95
  2. package/{HttpException.d.ts → dist/HttpException.d.ts} +2 -2
  3. package/{HttpException.js → dist/HttpException.js} +3 -3
  4. package/{StreamResponse.d.ts → dist/StreamJSONResponse.d.ts} +3 -3
  5. package/{StreamResponse.js → dist/StreamJSONResponse.js} +5 -5
  6. package/{Segment.d.ts → dist/VovkApp.d.ts} +11 -10
  7. package/{Segment.js → dist/VovkApp.js} +28 -24
  8. package/dist/client/createRPC.d.ts +4 -0
  9. package/{client/clientizeController.js → dist/client/createRPC.js} +22 -40
  10. package/dist/client/defaultFetcher.d.ts +4 -0
  11. package/{client → dist/client}/defaultFetcher.js +19 -8
  12. package/{client → dist/client}/defaultHandler.d.ts +1 -1
  13. package/dist/client/defaultHandler.js +22 -0
  14. package/dist/client/defaultStreamHandler.d.ts +4 -0
  15. package/{client → dist/client}/defaultStreamHandler.js +5 -5
  16. package/dist/client/index.d.ts +2 -0
  17. package/dist/client/index.js +8 -0
  18. package/dist/client/types.d.ts +103 -0
  19. package/dist/createDecorator.d.ts +4 -0
  20. package/{createDecorator.js → dist/createDecorator.js} +5 -4
  21. package/{createSegment.d.ts → dist/createVovkApp.d.ts} +10 -10
  22. package/{createSegment.js → dist/createVovkApp.js} +25 -25
  23. package/dist/index.d.ts +60 -0
  24. package/dist/index.js +24 -0
  25. package/dist/openapi/fromSchema.d.ts +3 -0
  26. package/dist/openapi/fromSchema.js +26 -0
  27. package/dist/openapi/index.d.ts +1 -0
  28. package/dist/openapi/index.js +5 -0
  29. package/dist/openapi/openapi.d.ts +10 -0
  30. package/dist/openapi/openapi.js +16 -0
  31. package/dist/types.d.ts +147 -0
  32. package/dist/types.js +65 -0
  33. package/dist/utils/generateStaticAPI.d.ts +4 -0
  34. package/{generateStaticAPI.js → dist/utils/generateStaticAPI.js} +3 -3
  35. package/{utils → dist/utils}/getSchema.d.ts +1 -2
  36. package/{utils → dist/utils}/getSchema.js +5 -16
  37. package/dist/utils/parseQuery.d.ts +25 -0
  38. package/dist/utils/parseQuery.js +156 -0
  39. package/dist/utils/reqForm.d.ts +2 -0
  40. package/dist/utils/reqForm.js +13 -0
  41. package/{utils → dist/utils}/reqMeta.d.ts +1 -2
  42. package/{utils → dist/utils}/reqQuery.d.ts +1 -2
  43. package/dist/utils/reqQuery.js +10 -0
  44. package/dist/utils/serializeQuery.d.ts +13 -0
  45. package/dist/utils/serializeQuery.js +65 -0
  46. package/dist/utils/setClientValidatorsForHandler.d.ts +5 -0
  47. package/{utils → dist/utils}/setClientValidatorsForHandler.js +4 -6
  48. package/package.json +8 -2
  49. package/src/HttpException.ts +16 -0
  50. package/src/StreamJSONResponse.ts +61 -0
  51. package/src/VovkApp.ts +240 -0
  52. package/src/client/createRPC.ts +120 -0
  53. package/src/client/defaultFetcher.ts +69 -0
  54. package/src/client/defaultHandler.ts +23 -0
  55. package/src/client/defaultStreamHandler.ts +88 -0
  56. package/src/client/index.ts +9 -0
  57. package/src/client/types.ts +120 -0
  58. package/src/createDecorator.ts +61 -0
  59. package/src/createVovkApp.ts +168 -0
  60. package/src/index.ts +71 -0
  61. package/src/openapi/fromSchema.ts +33 -0
  62. package/src/openapi/index.ts +1 -0
  63. package/src/openapi/openapi.ts +23 -0
  64. package/src/types.ts +200 -0
  65. package/src/utils/generateStaticAPI.ts +18 -0
  66. package/src/utils/getSchema.ts +35 -0
  67. package/src/utils/parseQuery.ts +160 -0
  68. package/src/utils/reqForm.ts +16 -0
  69. package/src/utils/reqMeta.ts +16 -0
  70. package/src/utils/reqQuery.ts +6 -0
  71. package/src/utils/serializeQuery.ts +69 -0
  72. package/src/utils/setClientValidatorsForHandler.ts +45 -0
  73. package/src/utils/shim.ts +17 -0
  74. package/.npmignore +0 -2
  75. package/client/clientizeController.d.ts +0 -4
  76. package/client/defaultFetcher.d.ts +0 -4
  77. package/client/defaultHandler.js +0 -21
  78. package/client/defaultStreamHandler.d.ts +0 -4
  79. package/client/index.d.ts +0 -4
  80. package/client/index.js +0 -5
  81. package/client/types.d.ts +0 -102
  82. package/createDecorator.d.ts +0 -4
  83. package/generateStaticAPI.d.ts +0 -4
  84. package/index.d.ts +0 -60
  85. package/index.js +0 -20
  86. package/types.d.ts +0 -191
  87. package/types.js +0 -65
  88. package/utils/reqQuery.js +0 -25
  89. package/utils/setClientValidatorsForHandler.d.ts +0 -5
  90. package/worker/index.d.ts +0 -3
  91. package/worker/index.js +0 -7
  92. package/worker/promisifyWorker.d.ts +0 -2
  93. package/worker/promisifyWorker.js +0 -143
  94. package/worker/types.d.ts +0 -31
  95. package/worker/types.js +0 -2
  96. package/worker/worker.d.ts +0 -1
  97. package/worker/worker.js +0 -44
  98. /package/{client → dist/client}/types.js +0 -0
  99. /package/{utils → dist/utils}/reqMeta.js +0 -0
  100. /package/{utils → dist/utils}/shim.d.ts +0 -0
  101. /package/{utils → dist/utils}/shim.js +0 -0
package/src/VovkApp.ts ADDED
@@ -0,0 +1,240 @@
1
+ import type { NextRequest } from 'next/server';
2
+ import {
3
+ HttpMethod,
4
+ HttpStatus,
5
+ type RouteHandler,
6
+ type VovkErrorResponse,
7
+ type VovkController,
8
+ type DecoratorOptions,
9
+ type KnownAny,
10
+ type VovkRequest,
11
+ } from './types';
12
+ import { HttpException } from './HttpException';
13
+ import { StreamJSONResponse } from './StreamJSONResponse';
14
+ import reqQuery from './utils/reqQuery';
15
+ import reqMeta from './utils/reqMeta';
16
+ import reqForm from './utils/reqForm';
17
+
18
+ export class VovkApp {
19
+ private static getHeadersFromOptions(options?: DecoratorOptions) {
20
+ if (!options) return {};
21
+
22
+ const corsHeaders = {
23
+ 'Access-Control-Allow-Origin': '*',
24
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, HEAD',
25
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
26
+ };
27
+
28
+ const headers = {
29
+ ...(options.cors ? corsHeaders : {}),
30
+ ...(options.headers ?? {}),
31
+ };
32
+
33
+ return headers;
34
+ }
35
+
36
+ routes: Record<HttpMethod, Map<VovkController, Record<string, RouteHandler>>> = {
37
+ GET: new Map(),
38
+ POST: new Map(),
39
+ PUT: new Map(),
40
+ PATCH: new Map(),
41
+ DELETE: new Map(),
42
+ HEAD: new Map(),
43
+ OPTIONS: new Map(),
44
+ };
45
+
46
+ GET = async (req: NextRequest, data: { params: Promise<Record<string, string[]>> }) =>
47
+ this.#callMethod(HttpMethod.GET, req, await data.params);
48
+
49
+ POST = async (req: NextRequest, data: { params: Promise<Record<string, string[]>> }) =>
50
+ this.#callMethod(HttpMethod.POST, req, await data.params);
51
+
52
+ PUT = async (req: NextRequest, data: { params: Promise<Record<string, string[]>> }) =>
53
+ this.#callMethod(HttpMethod.PUT, req, await data.params);
54
+
55
+ PATCH = async (req: NextRequest, data: { params: Promise<Record<string, string[]>> }) =>
56
+ this.#callMethod(HttpMethod.PATCH, req, await data.params);
57
+
58
+ DELETE = async (req: NextRequest, data: { params: Promise<Record<string, string[]>> }) =>
59
+ this.#callMethod(HttpMethod.DELETE, req, await data.params);
60
+
61
+ HEAD = async (req: NextRequest, data: { params: Promise<Record<string, string[]>> }) =>
62
+ this.#callMethod(HttpMethod.HEAD, req, await data.params);
63
+
64
+ OPTIONS = async (req: NextRequest, data: { params: Promise<Record<string, string[]>> }) =>
65
+ this.#callMethod(HttpMethod.OPTIONS, req, await data.params);
66
+
67
+ respond = (status: HttpStatus, body: unknown, options?: DecoratorOptions) => {
68
+ return new Response(JSON.stringify(body), {
69
+ status,
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ ...VovkApp.getHeadersFromOptions(options),
73
+ },
74
+ });
75
+ };
76
+
77
+ #respondWithError = (statusCode: HttpStatus, message: string, options?: DecoratorOptions, cause?: unknown) => {
78
+ return this.respond(
79
+ statusCode,
80
+ {
81
+ cause,
82
+ statusCode,
83
+ message,
84
+ isError: true,
85
+ } satisfies VovkErrorResponse,
86
+ options
87
+ );
88
+ };
89
+
90
+ #callMethod = async (httpMethod: HttpMethod, nextReq: NextRequest, params: Record<string, string[]>) => {
91
+ const req = nextReq as unknown as VovkRequest;
92
+ const controllers = this.routes[httpMethod];
93
+ const methodParams: Record<string, string> = {};
94
+ const path = params[Object.keys(params)[0]];
95
+
96
+ const handlers: Record<string, { staticMethod: RouteHandler; controller: VovkController }> = {};
97
+ controllers.forEach((staticMethods, controller) => {
98
+ const prefix = controller._prefix ?? '';
99
+
100
+ if (!controller._activated) {
101
+ throw new HttpException(
102
+ HttpStatus.INTERNAL_SERVER_ERROR,
103
+ `Controller "${controller.name}" found but not activated`
104
+ );
105
+ }
106
+
107
+ Object.entries(staticMethods).forEach(([path, staticMethod]) => {
108
+ const fullPath = [prefix, path].filter(Boolean).join('/');
109
+ handlers[fullPath] = { staticMethod, controller };
110
+ });
111
+ });
112
+
113
+ const getHandler = () => {
114
+ if (Object.keys(params).length === 0) {
115
+ return handlers[''];
116
+ }
117
+
118
+ const allMethodKeys = Object.keys(handlers);
119
+
120
+ let methodKeys: string[] = [];
121
+
122
+ methodKeys = allMethodKeys
123
+ // First, try to match literal routes exactly.
124
+ .filter((p) => {
125
+ if (p.includes(':')) return false; // Skip parameterized paths
126
+ return p === path.join('/');
127
+ });
128
+
129
+ if (!methodKeys.length) {
130
+ methodKeys = allMethodKeys.filter((p) => {
131
+ const routeSegments = p.split('/');
132
+ if (routeSegments.length !== path.length) return false;
133
+
134
+ for (let i = 0; i < routeSegments.length; i++) {
135
+ const routeSegment = routeSegments[i];
136
+ const pathSegment = path[i];
137
+
138
+ if (routeSegment.startsWith(':')) {
139
+ const parameter = routeSegment.slice(1);
140
+
141
+ if (parameter in methodParams) {
142
+ throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Duplicate parameter "${parameter}"`);
143
+ }
144
+
145
+ // If it's a parameterized segment, capture the parameter value.
146
+ methodParams[parameter] = pathSegment;
147
+ } else if (routeSegment !== pathSegment) {
148
+ // If it's a literal segment and it does not match the corresponding path segment, return false.
149
+ return false;
150
+ }
151
+ }
152
+ return true;
153
+ });
154
+ }
155
+
156
+ if (methodKeys.length > 1) {
157
+ throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, `Conflicting routes found: ${methodKeys.join(', ')}`);
158
+ }
159
+
160
+ const [methodKey] = methodKeys;
161
+
162
+ if (methodKey) {
163
+ return handlers[methodKey];
164
+ }
165
+
166
+ return null;
167
+ };
168
+
169
+ const handler = getHandler();
170
+
171
+ if (!handler) {
172
+ return this.#respondWithError(HttpStatus.NOT_FOUND, `Route ${path.join('/')} is not found`);
173
+ }
174
+
175
+ const { staticMethod, controller } = handler;
176
+
177
+ req.vovk = {
178
+ body: () => req.json(),
179
+ query: () => reqQuery(req as VovkRequest),
180
+ meta: <T = KnownAny>(meta?: T | null) => reqMeta<T>(req, meta),
181
+ form: <T = KnownAny>() => reqForm<T>(req),
182
+ };
183
+
184
+ try {
185
+ const result = await staticMethod.call(controller, req, methodParams);
186
+
187
+ const isIterator =
188
+ typeof result === 'object' &&
189
+ !!result &&
190
+ ((Reflect.has(result, Symbol.iterator) &&
191
+ typeof (result as Iterable<unknown>)[Symbol.iterator] === 'function') ||
192
+ (Reflect.has(result, Symbol.asyncIterator) &&
193
+ typeof (result as AsyncIterable<unknown>)[Symbol.asyncIterator] === 'function'));
194
+
195
+ if (isIterator && !(result instanceof Array)) {
196
+ const streamResponse = new StreamJSONResponse({
197
+ headers: {
198
+ ...StreamJSONResponse.defaultHeaders,
199
+ ...VovkApp.getHeadersFromOptions(staticMethod._options),
200
+ },
201
+ });
202
+
203
+ void (async () => {
204
+ try {
205
+ for await (const chunk of result as AsyncGenerator<unknown>) {
206
+ streamResponse.send(chunk);
207
+ }
208
+ } catch (e) {
209
+ return streamResponse.throw(e);
210
+ }
211
+
212
+ return streamResponse.close();
213
+ })();
214
+
215
+ return streamResponse;
216
+ }
217
+
218
+ if (result instanceof Response) {
219
+ return result;
220
+ }
221
+
222
+ return this.respond(200, result ?? null, staticMethod._options);
223
+ } catch (e) {
224
+ const err = e as HttpException;
225
+ try {
226
+ await controller._onError?.(err, req);
227
+ } catch (onErrorError) {
228
+ // eslint-disable-next-line no-console
229
+ console.error(onErrorError);
230
+ }
231
+
232
+ if (err.message !== 'NEXT_REDIRECT' && err.message !== 'NEXT_NOT_FOUND') {
233
+ const statusCode = err.statusCode ?? HttpStatus.INTERNAL_SERVER_ERROR;
234
+ return this.#respondWithError(statusCode, err.message, staticMethod._options, err.cause);
235
+ }
236
+
237
+ throw e; // if NEXT_REDIRECT or NEXT_NOT_FOUND, rethrow it
238
+ }
239
+ };
240
+ }
@@ -0,0 +1,120 @@
1
+ import {
2
+ type VovkControllerSchema,
3
+ type ControllerStaticMethod,
4
+ type VovkControllerParams,
5
+ type VovkControllerQuery,
6
+ type KnownAny,
7
+ type HttpMethod,
8
+ } from '../types';
9
+ import { type VovkClientOptions, type VovkClient, type VovkDefaultFetcherOptions, VovkValidateOnClient } from './types';
10
+
11
+ import defaultFetcher from './defaultFetcher';
12
+ import { defaultHandler } from './defaultHandler';
13
+ import { defaultStreamHandler } from './defaultStreamHandler';
14
+ import serializeQuery from '../utils/serializeQuery';
15
+
16
+ const trimPath = (path: string) => path.trim().replace(/^\/|\/$/g, '');
17
+
18
+ const getHandlerPath = <T extends ControllerStaticMethod>(
19
+ endpoint: string,
20
+ params?: VovkControllerParams<T>,
21
+ query?: VovkControllerQuery<T>
22
+ ) => {
23
+ let result = endpoint;
24
+ for (const [key, value] of Object.entries(params ?? {})) {
25
+ result = result.replace(`:${key}`, value as string);
26
+ }
27
+
28
+ const queryStr = query ? serializeQuery(query) : null;
29
+
30
+ return `${result}${queryStr ? '?' : ''}${queryStr}`;
31
+ };
32
+
33
+ const createRPC = <T, OPTS extends Record<string, KnownAny> = VovkDefaultFetcherOptions>(
34
+ controllerSchema: VovkControllerSchema,
35
+ segmentName?: string,
36
+ options?: VovkClientOptions<OPTS>
37
+ ): VovkClient<T, OPTS> => {
38
+ const schema = controllerSchema as T & VovkControllerSchema;
39
+ const client = {} as VovkClient<T, OPTS>;
40
+ if (!schema) throw new Error(`Unable to clientize. Controller schema is not provided`);
41
+ if (!schema.handlers)
42
+ throw new Error(`Unable to clientize. No schema for controller ${String(schema?.controllerName)} provided`);
43
+ const controllerPrefix = trimPath(schema.prefix ?? '');
44
+ const { fetcher: settingsFetcher = defaultFetcher } = options ?? {};
45
+
46
+ for (const [staticMethodName, handlerSchema] of Object.entries(schema.handlers)) {
47
+ const { path, httpMethod, validation } = handlerSchema;
48
+ const getEndpoint = ({
49
+ apiRoot,
50
+ params,
51
+ query,
52
+ }: {
53
+ apiRoot: string;
54
+ params: { [key: string]: string };
55
+ query: { [key: string]: string };
56
+ }) => {
57
+ const mainPrefix =
58
+ (apiRoot.startsWith('http://') || apiRoot.startsWith('https://') || apiRoot.startsWith('/') ? '' : '/') +
59
+ (apiRoot.endsWith('/') ? apiRoot : `${apiRoot}/`) +
60
+ (segmentName ? `${segmentName}/` : '');
61
+ return mainPrefix + getHandlerPath([controllerPrefix, path].filter(Boolean).join('/'), params, query);
62
+ };
63
+
64
+ const handler = (
65
+ input: {
66
+ body?: unknown;
67
+ query?: { [key: string]: string };
68
+ params?: { [key: string]: string };
69
+ validateOnClient?: VovkValidateOnClient;
70
+ fetcher?: VovkClientOptions<OPTS>['fetcher'];
71
+ transform?: (response: unknown) => unknown;
72
+ } & OPTS = {} as OPTS
73
+ ) => {
74
+ const fetcher = input.fetcher ?? settingsFetcher;
75
+ const validate = async ({ body, query, endpoint }: { body?: unknown; query?: unknown; endpoint: string }) => {
76
+ await (input.validateOnClient ?? options?.validateOnClient)?.({ body, query, endpoint }, validation ?? {});
77
+ };
78
+
79
+ const internalOptions: Parameters<typeof fetcher>[0] = {
80
+ name: staticMethodName as keyof T,
81
+ httpMethod: httpMethod as HttpMethod,
82
+ getEndpoint,
83
+ validate,
84
+ defaultHandler,
85
+ defaultStreamHandler,
86
+ };
87
+ const internalInput = {
88
+ ...options?.defaultOptions,
89
+ ...input,
90
+ body: input.body ?? null,
91
+ query: input.query ?? {},
92
+ params: input.params ?? {},
93
+ // TS workaround
94
+ fetcher: undefined,
95
+ validateOnClient: undefined,
96
+ };
97
+
98
+ delete internalInput.fetcher;
99
+ delete internalInput.validateOnClient;
100
+
101
+ if (!fetcher) throw new Error('Fetcher is not provided');
102
+
103
+ const fetcherPromise = fetcher(internalOptions, internalInput) as Promise<unknown>;
104
+
105
+ if (!(fetcherPromise instanceof Promise)) return Promise.resolve(fetcherPromise);
106
+
107
+ return input.transform ? fetcherPromise.then(input.transform) : fetcherPromise;
108
+ };
109
+
110
+ handler.schema = handlerSchema;
111
+ handler.controllerSchema = schema;
112
+
113
+ // @ts-expect-error TODO
114
+ client[staticMethodName] = handler;
115
+ }
116
+
117
+ return client;
118
+ };
119
+
120
+ export default createRPC;
@@ -0,0 +1,69 @@
1
+ import type { VovkDefaultFetcherOptions, VovkClientFetcher } from './types';
2
+ import { HttpStatus } from '../types';
3
+ import { HttpException } from '../HttpException';
4
+
5
+ export const DEFAULT_ERROR_MESSAGE = 'Unknown error at the defaultFetcher';
6
+
7
+ // defaultFetcher uses HttpException class to throw errors of fake HTTP status 0 if client-side error occurs
8
+ // For normal HTTP errors, it uses message and status code from the response of VovkErrorResponse type
9
+ const defaultFetcher: VovkClientFetcher<VovkDefaultFetcherOptions> = async (
10
+ { httpMethod, getEndpoint, validate, defaultHandler, defaultStreamHandler },
11
+ { params, query, body, apiRoot = '/api', ...options }
12
+ ) => {
13
+ const endpoint = getEndpoint({ apiRoot, params, query });
14
+
15
+ if (!options.disableClientValidation) {
16
+ try {
17
+ await validate({ body, query, endpoint });
18
+ } catch (e) {
19
+ // if HttpException is thrown, rethrow it
20
+ if (e instanceof HttpException) throw e;
21
+ // otherwise, throw HttpException with status 0
22
+ throw new HttpException(HttpStatus.NULL, (e as Error).message ?? DEFAULT_ERROR_MESSAGE, {
23
+ body,
24
+ query,
25
+ params,
26
+ endpoint,
27
+ });
28
+ }
29
+ }
30
+
31
+ const init: RequestInit = {
32
+ method: httpMethod,
33
+ ...options,
34
+ };
35
+
36
+ if (body instanceof FormData) {
37
+ init.body = body as BodyInit;
38
+ } else if (body) {
39
+ init.body = JSON.stringify(body);
40
+ }
41
+
42
+ let response: Response;
43
+
44
+ try {
45
+ response = await fetch(endpoint, init);
46
+ } catch (e) {
47
+ // handle network errors
48
+ throw new HttpException(HttpStatus.NULL, (e as Error)?.message ?? DEFAULT_ERROR_MESSAGE, {
49
+ body,
50
+ query,
51
+ params,
52
+ endpoint,
53
+ });
54
+ }
55
+
56
+ const contentType = response.headers.get('content-type');
57
+
58
+ if (contentType?.startsWith('application/json')) {
59
+ return defaultHandler(response);
60
+ }
61
+
62
+ if (contentType === 'text/plain; format=jsonlines') {
63
+ return defaultStreamHandler(response);
64
+ }
65
+
66
+ return response;
67
+ };
68
+
69
+ export default defaultFetcher;
@@ -0,0 +1,23 @@
1
+ import { type VovkErrorResponse } from '../types';
2
+ import { HttpException } from '../HttpException';
3
+
4
+ export const DEFAULT_ERROR_MESSAGE = 'Unknown error at defaultHandler';
5
+
6
+ export const defaultHandler = async (response: Response) => {
7
+ let result: unknown;
8
+
9
+ try {
10
+ result = await response.json();
11
+ } catch (e) {
12
+ // handle parsing errors
13
+ throw new HttpException(response.status, (e as Error)?.message ?? DEFAULT_ERROR_MESSAGE);
14
+ }
15
+
16
+ if (!response.ok) {
17
+ // handle server errors
18
+ const errorResponse = result as VovkErrorResponse;
19
+ throw new HttpException(response.status, errorResponse?.message ?? DEFAULT_ERROR_MESSAGE, errorResponse?.cause);
20
+ }
21
+
22
+ return result;
23
+ };
@@ -0,0 +1,88 @@
1
+ import { HttpStatus, type VovkErrorResponse } from '../types';
2
+ import type { VovkStreamAsyncIterable } from './types';
3
+ import { HttpException } from '../HttpException';
4
+ import '../utils/shim';
5
+
6
+ export const DEFAULT_ERROR_MESSAGE = 'Unknown error at defaultStreamHandler';
7
+
8
+ export const defaultStreamHandler = async (response: Response): Promise<VovkStreamAsyncIterable<unknown>> => {
9
+ if (!response.ok) {
10
+ let result: unknown;
11
+ try {
12
+ result = await response.json();
13
+ } catch {
14
+ // ignore parsing errors
15
+ }
16
+ // handle server errors
17
+ throw new HttpException(response.status, (result as VovkErrorResponse).message ?? DEFAULT_ERROR_MESSAGE);
18
+ }
19
+
20
+ if (!response.body) throw new HttpException(HttpStatus.NULL, 'Stream body is falsy. Check your controller code.');
21
+
22
+ const reader = response.body.getReader();
23
+
24
+ // if streaming is too rapid, we need to make sure that the loop is stopped
25
+ let canceled = false;
26
+
27
+ async function* asyncIterator() {
28
+ let prepend = '';
29
+
30
+ while (true) {
31
+ let value: Uint8Array | undefined;
32
+ let done = false;
33
+
34
+ try {
35
+ ({ value, done } = await reader.read());
36
+ } catch (error) {
37
+ await reader.cancel();
38
+ const err = new Error('Stream error. ' + String(error));
39
+ err.cause = error;
40
+ throw err;
41
+ }
42
+
43
+ if (done) {
44
+ return;
45
+ }
46
+
47
+ // typeof value === 'number' is a workaround for React Native
48
+ const string = typeof value === 'number' ? String.fromCharCode(value) : new TextDecoder().decode(value);
49
+ prepend += string;
50
+ const lines = prepend.split('\n').filter(Boolean);
51
+ for (const line of lines) {
52
+ let data;
53
+ try {
54
+ data = JSON.parse(line) as object;
55
+ prepend = '';
56
+ } catch {
57
+ break;
58
+ }
59
+
60
+ if (data) {
61
+ if ('isError' in data && 'reason' in data) {
62
+ const upcomingError = data.reason;
63
+ await reader.cancel();
64
+
65
+ if (typeof upcomingError === 'string') {
66
+ throw new Error(upcomingError);
67
+ }
68
+
69
+ throw upcomingError;
70
+ } else if (!canceled) {
71
+ yield data;
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ return {
79
+ status: response.status,
80
+ [Symbol.asyncIterator]: asyncIterator,
81
+ [Symbol.dispose]: () => reader.cancel(),
82
+ [Symbol.asyncDispose]: () => reader.cancel(),
83
+ cancel: () => {
84
+ canceled = true;
85
+ return reader.cancel();
86
+ },
87
+ };
88
+ };
@@ -0,0 +1,9 @@
1
+ export { default as createRPC } from './createRPC';
2
+ export type {
3
+ VovkClient,
4
+ VovkClientFetcher,
5
+ VovkClientOptions,
6
+ VovkDefaultFetcherOptions,
7
+ VovkValidateOnClient,
8
+ VovkStreamAsyncIterable,
9
+ } from './types';
@@ -0,0 +1,120 @@
1
+ import type {
2
+ KnownAny,
3
+ HttpMethod,
4
+ ControllerStaticMethod,
5
+ VovkControllerBody,
6
+ VovkControllerQuery,
7
+ VovkControllerParams,
8
+ VovkHandlerSchema,
9
+ VovkControllerSchema,
10
+ } from '../types';
11
+ import type { StreamJSONResponse } from '../StreamJSONResponse';
12
+ import type { NextResponse } from 'next/server';
13
+
14
+ export type StaticMethodInput<T extends ControllerStaticMethod> = (VovkControllerBody<T> extends undefined | void
15
+ ? { body?: undefined }
16
+ : VovkControllerBody<T> extends null
17
+ ? { body?: null }
18
+ : { body: VovkControllerBody<T> }) &
19
+ (VovkControllerQuery<T> extends undefined | void ? { query?: undefined } : { query: VovkControllerQuery<T> }) &
20
+ (VovkControllerParams<T> extends undefined | void ? { params?: undefined } : { params: VovkControllerParams<T> });
21
+
22
+ type ToPromise<T> = T extends PromiseLike<unknown> ? T : Promise<T>;
23
+
24
+ export type VovkStreamAsyncIterable<T> = {
25
+ status: number;
26
+ [Symbol.dispose](): Promise<void> | void;
27
+ [Symbol.asyncDispose](): Promise<void> | void;
28
+ [Symbol.asyncIterator](): AsyncIterator<T>;
29
+ cancel: () => Promise<void> | void;
30
+ };
31
+
32
+ type StaticMethodReturn<T extends ControllerStaticMethod> =
33
+ ReturnType<T> extends NextResponse<infer U> | Promise<NextResponse<infer U>>
34
+ ? U
35
+ : ReturnType<T> extends Response | Promise<Response>
36
+ ? Awaited<ReturnType<T>>
37
+ : ReturnType<T>;
38
+
39
+ type StaticMethodReturnPromise<T extends ControllerStaticMethod> = ToPromise<StaticMethodReturn<T>>;
40
+
41
+ type ClientMethod<
42
+ T extends (...args: KnownAny[]) => void | object | StreamJSONResponse<STREAM> | Promise<StreamJSONResponse<STREAM>>,
43
+ OPTS extends Record<string, KnownAny>,
44
+ STREAM extends KnownAny = unknown,
45
+ > = (<R>(
46
+ options: (StaticMethodInput<T> extends { body?: undefined | null; query?: undefined; params?: undefined }
47
+ ? unknown
48
+ : Parameters<T>[0] extends void
49
+ ? StaticMethodInput<T>['params'] extends object
50
+ ? { params: StaticMethodInput<T>['params'] }
51
+ : unknown
52
+ : StaticMethodInput<T>) &
53
+ (Partial<
54
+ OPTS & {
55
+ transform: (staticMethodReturn: Awaited<StaticMethodReturn<T>>) => R;
56
+ }
57
+ > | void)
58
+ ) => ReturnType<T> extends
59
+ | Promise<StreamJSONResponse<infer U>>
60
+ | StreamJSONResponse<infer U>
61
+ | Iterator<infer U>
62
+ | AsyncIterator<infer U>
63
+ ? Promise<VovkStreamAsyncIterable<U>>
64
+ : R extends object
65
+ ? Promise<R>
66
+ : StaticMethodReturnPromise<T>) & {
67
+ schema: VovkHandlerSchema;
68
+ controllerSchema: VovkControllerSchema;
69
+ };
70
+
71
+ type OmitNever<T> = {
72
+ [K in keyof T as T[K] extends never ? never : K]: T[K];
73
+ };
74
+
75
+ type VovkClientWithNever<T, OPTS extends { [key: string]: KnownAny }> = {
76
+ [K in keyof T]: T[K] extends (...args: KnownAny) => KnownAny ? ClientMethod<T[K], OPTS> : never;
77
+ };
78
+
79
+ export type VovkClient<T, OPTS extends { [key: string]: KnownAny }> = OmitNever<VovkClientWithNever<T, OPTS>>;
80
+
81
+ export type VovkClientFetcher<OPTS extends Record<string, KnownAny> = Record<string, never>, T = KnownAny> = (
82
+ options: {
83
+ name: keyof T;
84
+ httpMethod: HttpMethod;
85
+ getEndpoint: (data: {
86
+ apiRoot: string;
87
+ params: { [key: string]: string };
88
+ query: { [key: string]: string };
89
+ }) => string;
90
+ validate: (input: { body?: unknown; query?: unknown; endpoint: string }) => void | Promise<void>;
91
+ defaultStreamHandler: (response: Response) => Promise<VovkStreamAsyncIterable<unknown>>;
92
+ defaultHandler: (response: Response) => Promise<unknown>;
93
+ },
94
+ input: {
95
+ body: unknown;
96
+ query: { [key: string]: string };
97
+ params: { [key: string]: string };
98
+ } & OPTS
99
+ ) => KnownAny;
100
+
101
+ // `RequestInit` is the type of options passed to fetch function
102
+ export interface VovkDefaultFetcherOptions extends Omit<RequestInit, 'body' | 'method'> {
103
+ reactNative?: { textStreaming: boolean };
104
+ apiRoot?: string;
105
+ segmentName?: string;
106
+ disableClientValidation?: boolean;
107
+ validateOnClient?: VovkValidateOnClient;
108
+ fetcher?: VovkClientFetcher;
109
+ }
110
+
111
+ export type VovkValidateOnClient = (
112
+ input: { body?: unknown; query?: unknown; endpoint: string },
113
+ validators: { body?: unknown; query?: unknown }
114
+ ) => void | Promise<void>;
115
+
116
+ export type VovkClientOptions<OPTS extends Record<string, KnownAny> = Record<string, never>> = {
117
+ fetcher?: VovkClientFetcher<OPTS>;
118
+ validateOnClient?: VovkValidateOnClient;
119
+ defaultOptions?: Partial<OPTS>;
120
+ };