astro-routify 1.0.0 → 1.2.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/README.md +160 -36
- package/dist/core/RouterBuilder.d.ts +114 -3
- package/dist/core/RouterBuilder.js +133 -6
- package/dist/core/defineGroup.d.ts +87 -0
- package/dist/core/defineGroup.js +111 -0
- package/dist/core/defineHandler.d.ts +23 -0
- package/dist/core/defineHandler.js +40 -0
- package/dist/core/defineRoute.d.ts +32 -0
- package/dist/core/defineRoute.js +14 -1
- package/dist/core/defineRouter.d.ts +33 -1
- package/dist/core/defineRouter.js +32 -3
- package/dist/core/internal/createJsonStreamRoute.d.ts +64 -0
- package/dist/core/internal/createJsonStreamRoute.js +92 -0
- package/dist/core/responseHelpers.d.ts +63 -1
- package/dist/core/responseHelpers.js +68 -0
- package/dist/core/stream.d.ts +75 -0
- package/dist/core/stream.js +91 -0
- package/dist/core/streamJsonArray.d.ts +31 -0
- package/dist/core/streamJsonArray.js +30 -0
- package/dist/core/streamJsonND.d.ts +34 -0
- package/dist/core/streamJsonND.js +33 -0
- package/dist/index.d.ts +512 -6
- package/dist/index.js +4 -0
- package/package.json +13 -7
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { HttpMethod } from './HttpMethod';
|
|
2
|
+
import { defineRoute } from './defineRoute';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a group of routes under a shared base path.
|
|
5
|
+
*
|
|
6
|
+
* Use this class to organize related endpoints, applying a consistent prefix
|
|
7
|
+
* and reducing duplication when defining similar routes.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const users = new RouteGroup('/users')
|
|
11
|
+
* .addGet('/:id', handler)
|
|
12
|
+
* .addPost('/', createUser);
|
|
13
|
+
*/
|
|
14
|
+
export class RouteGroup {
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new route group with the specified base path.
|
|
17
|
+
* Trailing slashes are automatically removed.
|
|
18
|
+
*
|
|
19
|
+
* @param basePath - The common prefix for all routes in the group (e.g. "/users")
|
|
20
|
+
*/
|
|
21
|
+
constructor(basePath) {
|
|
22
|
+
this.routes = [];
|
|
23
|
+
this.basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns the normalized base path used by the group.
|
|
27
|
+
*/
|
|
28
|
+
getBasePath() {
|
|
29
|
+
return this.basePath;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Registers a GET route under the group's base path.
|
|
33
|
+
*
|
|
34
|
+
* @param path - Path relative to the base path (e.g. "/:id")
|
|
35
|
+
* @param handler - The handler function for this route
|
|
36
|
+
*/
|
|
37
|
+
addGet(path, handler) {
|
|
38
|
+
return this.add(HttpMethod.GET, path, handler);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Registers a POST route under the group's base path.
|
|
42
|
+
*
|
|
43
|
+
* @param path - Path relative to the base path
|
|
44
|
+
* @param handler - The handler function for this route
|
|
45
|
+
*/
|
|
46
|
+
addPost(path, handler) {
|
|
47
|
+
return this.add(HttpMethod.POST, path, handler);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Registers a PUT route under the group's base path.
|
|
51
|
+
*
|
|
52
|
+
* @param path - Path relative to the base path
|
|
53
|
+
* @param handler - The handler function for this route
|
|
54
|
+
*/
|
|
55
|
+
addPut(path, handler) {
|
|
56
|
+
return this.add(HttpMethod.PUT, path, handler);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Registers a DELETE route under the group's base path.
|
|
60
|
+
*
|
|
61
|
+
* @param path - Path relative to the base path
|
|
62
|
+
* @param handler - The handler function for this route
|
|
63
|
+
*/
|
|
64
|
+
addDelete(path, handler) {
|
|
65
|
+
return this.add(HttpMethod.DELETE, path, handler);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Registers a PATCH route under the group's base path.
|
|
69
|
+
*
|
|
70
|
+
* @param path - Path relative to the base path
|
|
71
|
+
* @param handler - The handler function for this route
|
|
72
|
+
*/
|
|
73
|
+
addPatch(path, handler) {
|
|
74
|
+
return this.add(HttpMethod.PATCH, path, handler);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Internal method to register a route under the group with any HTTP method.
|
|
78
|
+
*
|
|
79
|
+
* @param method - HTTP verb
|
|
80
|
+
* @param subPath - Route path relative to the base
|
|
81
|
+
* @param handler - The handler function for this route
|
|
82
|
+
*/
|
|
83
|
+
add(method, subPath, handler) {
|
|
84
|
+
const normalizedPath = subPath.startsWith('/') ? subPath : `/${subPath}`;
|
|
85
|
+
this.routes.push(defineRoute(method, `${this.basePath}${normalizedPath}`, handler));
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Returns all the registered routes in the group.
|
|
90
|
+
*/
|
|
91
|
+
getRoutes() {
|
|
92
|
+
return this.routes;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Helper to define a `RouteGroup` with optional inline configuration.
|
|
97
|
+
*
|
|
98
|
+
* @param basePath - The base path prefix for all routes
|
|
99
|
+
* @param configure - Optional callback to configure the group inline
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* const users = defineGroup('/users', (group) => {
|
|
103
|
+
* group.addGet('/:id', handler);
|
|
104
|
+
* });
|
|
105
|
+
*/
|
|
106
|
+
export function defineGroup(basePath, configure) {
|
|
107
|
+
const group = new RouteGroup(basePath);
|
|
108
|
+
if (configure)
|
|
109
|
+
configure(group);
|
|
110
|
+
return group;
|
|
111
|
+
}
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
import type { APIContext, APIRoute } from 'astro';
|
|
2
2
|
import { type ResultResponse } from './responseHelpers';
|
|
3
|
+
/**
|
|
4
|
+
* A flexible route handler that can return:
|
|
5
|
+
* - a native `Response` object,
|
|
6
|
+
* - a structured `ResultResponse` object,
|
|
7
|
+
* - or a file stream (Blob, ArrayBuffer, or ReadableStream).
|
|
8
|
+
*/
|
|
3
9
|
export type Handler = (ctx: APIContext) => Promise<ResultResponse | Response> | ResultResponse | Response;
|
|
10
|
+
/**
|
|
11
|
+
* Wraps a `Handler` function into an `APIRoute` that:
|
|
12
|
+
* - logs requests and responses,
|
|
13
|
+
* - supports all valid `ResultResponse` return formats,
|
|
14
|
+
* - auto-converts structured responses into Astro `Response`s,
|
|
15
|
+
* - handles errors with standardized 500 output.
|
|
16
|
+
*
|
|
17
|
+
* @param handler - A handler function returning a `Response` or `ResultResponse`
|
|
18
|
+
* @returns An Astro-compatible `APIRoute` function
|
|
19
|
+
*/
|
|
4
20
|
export declare function defineHandler(handler: Handler): APIRoute;
|
|
21
|
+
/**
|
|
22
|
+
* Type guard to detect ReadableStreams, used for streamed/binary responses.
|
|
23
|
+
*
|
|
24
|
+
* @param value - Any value to test
|
|
25
|
+
* @returns True if it looks like a ReadableStream
|
|
26
|
+
*/
|
|
27
|
+
export declare function isReadableStream(value: unknown): value is ReadableStream<Uint8Array>;
|
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
import { internalError, toAstroResponse } from './responseHelpers';
|
|
2
|
+
/**
|
|
3
|
+
* Logs the incoming request method and path to the console.
|
|
4
|
+
*/
|
|
2
5
|
function logRequest(ctx) {
|
|
3
6
|
const { method, url } = ctx.request;
|
|
4
7
|
console.info(`[astro-routify] → ${method} ${new URL(url).pathname}`);
|
|
5
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Logs the response status and time taken.
|
|
11
|
+
*/
|
|
6
12
|
function logResponse(status, start) {
|
|
7
13
|
console.info(`[astro-routify] ← responded ${status} in ${Math.round(performance.now() - start)}ms`);
|
|
8
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a `Handler` function into an `APIRoute` that:
|
|
17
|
+
* - logs requests and responses,
|
|
18
|
+
* - supports all valid `ResultResponse` return formats,
|
|
19
|
+
* - auto-converts structured responses into Astro `Response`s,
|
|
20
|
+
* - handles errors with standardized 500 output.
|
|
21
|
+
*
|
|
22
|
+
* @param handler - A handler function returning a `Response` or `ResultResponse`
|
|
23
|
+
* @returns An Astro-compatible `APIRoute` function
|
|
24
|
+
*/
|
|
9
25
|
export function defineHandler(handler) {
|
|
10
26
|
return async (ctx) => {
|
|
11
27
|
const start = performance.now();
|
|
12
28
|
try {
|
|
13
29
|
logRequest(ctx);
|
|
14
30
|
const result = await handler(ctx);
|
|
31
|
+
// Native Response shortcut
|
|
15
32
|
if (result instanceof Response) {
|
|
16
33
|
logResponse(result.status, start);
|
|
17
34
|
return result;
|
|
18
35
|
}
|
|
36
|
+
// Direct binary or stream body (file download, etc.)
|
|
37
|
+
if (result?.body instanceof Blob ||
|
|
38
|
+
result?.body instanceof ArrayBuffer ||
|
|
39
|
+
isReadableStream(result?.body)) {
|
|
40
|
+
const res = new Response(result.body, {
|
|
41
|
+
status: result.status,
|
|
42
|
+
headers: result.headers,
|
|
43
|
+
});
|
|
44
|
+
logResponse(res.status, start);
|
|
45
|
+
return res;
|
|
46
|
+
}
|
|
47
|
+
// Structured ResultResponse → native Astro Response
|
|
19
48
|
const finalResponse = toAstroResponse(result);
|
|
20
49
|
logResponse(finalResponse.status, start);
|
|
21
50
|
return finalResponse;
|
|
@@ -28,3 +57,14 @@ export function defineHandler(handler) {
|
|
|
28
57
|
}
|
|
29
58
|
};
|
|
30
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Type guard to detect ReadableStreams, used for streamed/binary responses.
|
|
62
|
+
*
|
|
63
|
+
* @param value - Any value to test
|
|
64
|
+
* @returns True if it looks like a ReadableStream
|
|
65
|
+
*/
|
|
66
|
+
export function isReadableStream(value) {
|
|
67
|
+
return (typeof value === 'object' &&
|
|
68
|
+
value !== null &&
|
|
69
|
+
typeof value.getReader === 'function');
|
|
70
|
+
}
|
|
@@ -1,9 +1,41 @@
|
|
|
1
1
|
import { HttpMethod } from './HttpMethod';
|
|
2
2
|
import type { Handler } from './defineHandler';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a single route definition.
|
|
5
|
+
*/
|
|
3
6
|
export interface Route {
|
|
7
|
+
/**
|
|
8
|
+
* HTTP method to match (GET, POST, PUT, etc.).
|
|
9
|
+
*/
|
|
4
10
|
method: HttpMethod;
|
|
11
|
+
/**
|
|
12
|
+
* Path pattern, starting with `/`, supporting static or param segments (e.g., `/users/:id`).
|
|
13
|
+
*/
|
|
5
14
|
path: string;
|
|
15
|
+
/**
|
|
16
|
+
* The function that handles the request when matched.
|
|
17
|
+
*/
|
|
6
18
|
handler: Handler;
|
|
7
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Defines a route using a `Route` object.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* defineRoute({ method: 'GET', path: '/users', handler });
|
|
25
|
+
*
|
|
26
|
+
* @param route - A fully constructed route object
|
|
27
|
+
* @returns The validated Route object
|
|
28
|
+
*/
|
|
8
29
|
export declare function defineRoute(route: Route): Route;
|
|
30
|
+
/**
|
|
31
|
+
* Defines a route by specifying its method, path, and handler explicitly.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* defineRoute('POST', '/login', handler);
|
|
35
|
+
*
|
|
36
|
+
* @param method - HTTP method to match
|
|
37
|
+
* @param path - Route path (must start with `/`)
|
|
38
|
+
* @param handler - Function to handle the matched request
|
|
39
|
+
* @returns The validated Route object
|
|
40
|
+
*/
|
|
9
41
|
export declare function defineRoute(method: HttpMethod, path: string, handler: Handler): Route;
|
package/dist/core/defineRoute.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { ALLOWED_HTTP_METHODS } from './HttpMethod';
|
|
2
|
+
/**
|
|
3
|
+
* Internal route definition logic that supports both overloads.
|
|
4
|
+
*/
|
|
2
5
|
export function defineRoute(methodOrRoute, maybePath, maybeHandler) {
|
|
3
6
|
if (typeof methodOrRoute === 'object') {
|
|
4
7
|
validateRoute(methodOrRoute);
|
|
5
8
|
return methodOrRoute;
|
|
6
9
|
}
|
|
7
|
-
const route = {
|
|
10
|
+
const route = {
|
|
11
|
+
method: methodOrRoute,
|
|
12
|
+
path: maybePath,
|
|
13
|
+
handler: maybeHandler,
|
|
14
|
+
};
|
|
8
15
|
validateRoute(route);
|
|
9
16
|
return route;
|
|
10
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Ensures the route is properly formed and uses a valid method + path format.
|
|
20
|
+
*
|
|
21
|
+
* @param route - Route to validate
|
|
22
|
+
* @throws If method is unsupported or path doesn't start with `/`
|
|
23
|
+
*/
|
|
11
24
|
function validateRoute({ method, path }) {
|
|
12
25
|
if (!path.startsWith('/')) {
|
|
13
26
|
throw new Error(`Route path must start with '/': ${path}`);
|
|
@@ -1,8 +1,40 @@
|
|
|
1
1
|
import type { APIRoute } from 'astro';
|
|
2
2
|
import { notFound } from './responseHelpers';
|
|
3
3
|
import type { Route } from './defineRoute';
|
|
4
|
+
/**
|
|
5
|
+
* Optional configuration for the router instance.
|
|
6
|
+
*/
|
|
4
7
|
export interface RouterOptions {
|
|
5
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* A base path to strip from the incoming request path (default: `/api`).
|
|
10
|
+
* Only routes beneath this prefix will be matched.
|
|
11
|
+
*/
|
|
12
|
+
basePath?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Custom handler to return when no route is matched (404).
|
|
15
|
+
*/
|
|
6
16
|
onNotFound?: () => ReturnType<typeof notFound>;
|
|
7
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Defines a router that dynamically matches registered routes based on method and path.
|
|
20
|
+
*
|
|
21
|
+
* This allows building a clean, centralized API routing system with features like:
|
|
22
|
+
* - Trie-based fast route lookup
|
|
23
|
+
* - Per-method matching with 405 fallback
|
|
24
|
+
* - Parameter extraction (e.g. `/users/:id`)
|
|
25
|
+
* - Customizable basePath and 404 behavior
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* defineRouter([
|
|
29
|
+
* defineRoute('GET', '/users', handler),
|
|
30
|
+
* defineRoute('POST', '/login', loginHandler),
|
|
31
|
+
* ], {
|
|
32
|
+
* basePath: '/api',
|
|
33
|
+
* onNotFound: () => notFound('No such route')
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* @param routes - An array of route definitions (see `defineRoute`)
|
|
37
|
+
* @param options - Optional router config (basePath, custom 404)
|
|
38
|
+
* @returns An Astro-compatible APIRoute handler
|
|
39
|
+
*/
|
|
8
40
|
export declare function defineRouter(routes: Route[], options?: RouterOptions): APIRoute;
|
|
@@ -2,26 +2,55 @@ import { defineHandler } from './defineHandler';
|
|
|
2
2
|
import { methodNotAllowed, notFound, toAstroResponse } from './responseHelpers';
|
|
3
3
|
import { RouteTrie } from './RouteTrie';
|
|
4
4
|
import { normalizeMethod } from './HttpMethod';
|
|
5
|
+
/**
|
|
6
|
+
* Defines a router that dynamically matches registered routes based on method and path.
|
|
7
|
+
*
|
|
8
|
+
* This allows building a clean, centralized API routing system with features like:
|
|
9
|
+
* - Trie-based fast route lookup
|
|
10
|
+
* - Per-method matching with 405 fallback
|
|
11
|
+
* - Parameter extraction (e.g. `/users/:id`)
|
|
12
|
+
* - Customizable basePath and 404 behavior
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* defineRouter([
|
|
16
|
+
* defineRoute('GET', '/users', handler),
|
|
17
|
+
* defineRoute('POST', '/login', loginHandler),
|
|
18
|
+
* ], {
|
|
19
|
+
* basePath: '/api',
|
|
20
|
+
* onNotFound: () => notFound('No such route')
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* @param routes - An array of route definitions (see `defineRoute`)
|
|
24
|
+
* @param options - Optional router config (basePath, custom 404)
|
|
25
|
+
* @returns An Astro-compatible APIRoute handler
|
|
26
|
+
*/
|
|
5
27
|
export function defineRouter(routes, options = {}) {
|
|
6
28
|
const trie = new RouteTrie();
|
|
7
29
|
for (const route of routes) {
|
|
8
30
|
trie.insert(route.path, route.method, route.handler);
|
|
9
31
|
}
|
|
10
|
-
// Wrap every user handler through defineHandler for uniform logging & error handling
|
|
11
32
|
return async (ctx) => {
|
|
12
|
-
const
|
|
33
|
+
const pathname = new URL(ctx.request.url).pathname;
|
|
34
|
+
let basePath = options.basePath ?? '/api';
|
|
35
|
+
if (!basePath.startsWith('/')) {
|
|
36
|
+
basePath = '/' + basePath;
|
|
37
|
+
}
|
|
38
|
+
const basePathRegex = new RegExp(`^${basePath}`);
|
|
39
|
+
const path = pathname.replace(basePathRegex, '');
|
|
13
40
|
const method = normalizeMethod(ctx.request.method);
|
|
14
41
|
const { handler, allowed, params } = trie.find(path, method);
|
|
15
42
|
if (!handler) {
|
|
16
|
-
//
|
|
43
|
+
// Method exists but not allowed for this route
|
|
17
44
|
if (allowed && allowed.length) {
|
|
18
45
|
return toAstroResponse(methodNotAllowed('Method Not Allowed', {
|
|
19
46
|
Allow: allowed.join(', '),
|
|
20
47
|
}));
|
|
21
48
|
}
|
|
49
|
+
// No route matched at all → 404
|
|
22
50
|
const notFoundHandler = options.onNotFound ? options.onNotFound() : notFound('Not Found');
|
|
23
51
|
return toAstroResponse(notFoundHandler);
|
|
24
52
|
}
|
|
53
|
+
// Match found → delegate to handler
|
|
25
54
|
return defineHandler(handler)({ ...ctx, params: { ...ctx.params, ...params } });
|
|
26
55
|
};
|
|
27
56
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { APIContext } from 'astro';
|
|
2
|
+
import { HttpMethod } from '../HttpMethod';
|
|
3
|
+
import { type Route } from '../defineRoute';
|
|
4
|
+
type JsonValue = any;
|
|
5
|
+
/**
|
|
6
|
+
* A writer interface for streaming JSON data to the response body.
|
|
7
|
+
* Supports both NDJSON and array formats.
|
|
8
|
+
*/
|
|
9
|
+
export interface JsonStreamWriter {
|
|
10
|
+
/**
|
|
11
|
+
* Send a JSON-serializable value to the response stream.
|
|
12
|
+
* - In `ndjson` mode: appends a newline after each object.
|
|
13
|
+
* - In `array` mode: adds commas and wraps with brackets.
|
|
14
|
+
* @param value - Any serializable object or array item
|
|
15
|
+
*/
|
|
16
|
+
send: (value: JsonValue) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Write raw text or bytes to the stream.
|
|
19
|
+
* Used for low-level control if needed.
|
|
20
|
+
*/
|
|
21
|
+
write: (chunk: string | Uint8Array) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Close the stream. In `array` mode, it writes the closing `]`.
|
|
24
|
+
*/
|
|
25
|
+
close: () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Override response headers dynamically before the response is sent.
|
|
28
|
+
* Only safe before the first write.
|
|
29
|
+
* @param key - Header name
|
|
30
|
+
* @param value - Header value
|
|
31
|
+
*/
|
|
32
|
+
setHeader: (key: string, value: string) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Internal configuration options for `createJsonStreamRoute`.
|
|
36
|
+
*/
|
|
37
|
+
interface JsonStreamOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Streaming mode: 'ndjson' or 'array'.
|
|
40
|
+
*/
|
|
41
|
+
mode: 'ndjson' | 'array';
|
|
42
|
+
/**
|
|
43
|
+
* HTTP method to define (e.g. GET, POST).
|
|
44
|
+
*/
|
|
45
|
+
method: HttpMethod;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Creates a streaming JSON route that supports both NDJSON and JSON array formats.
|
|
49
|
+
*
|
|
50
|
+
* - Sets appropriate `Content-Type` headers
|
|
51
|
+
* - Supports cancellation via `AbortSignal`
|
|
52
|
+
* - Provides a `response` object with `send`, `close`, `write`, `setHeader` methods
|
|
53
|
+
*
|
|
54
|
+
* Use this function inside `streamJsonND()` or `streamJsonArray()` to avoid duplication.
|
|
55
|
+
*
|
|
56
|
+
* @param path - The route path (e.g. `/logs`)
|
|
57
|
+
* @param handler - A function that receives Astro `ctx` and a `JsonStreamWriter`
|
|
58
|
+
* @param options - Streaming options (`mode`, `method`)
|
|
59
|
+
* @returns A streaming-compatible Route
|
|
60
|
+
*/
|
|
61
|
+
export declare function createJsonStreamRoute(path: string, handler: (ctx: APIContext & {
|
|
62
|
+
response: JsonStreamWriter;
|
|
63
|
+
}) => void | Promise<void>, options: JsonStreamOptions): Route;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { defineRoute } from '../defineRoute';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a streaming JSON route that supports both NDJSON and JSON array formats.
|
|
4
|
+
*
|
|
5
|
+
* - Sets appropriate `Content-Type` headers
|
|
6
|
+
* - Supports cancellation via `AbortSignal`
|
|
7
|
+
* - Provides a `response` object with `send`, `close`, `write`, `setHeader` methods
|
|
8
|
+
*
|
|
9
|
+
* Use this function inside `streamJsonND()` or `streamJsonArray()` to avoid duplication.
|
|
10
|
+
*
|
|
11
|
+
* @param path - The route path (e.g. `/logs`)
|
|
12
|
+
* @param handler - A function that receives Astro `ctx` and a `JsonStreamWriter`
|
|
13
|
+
* @param options - Streaming options (`mode`, `method`)
|
|
14
|
+
* @returns A streaming-compatible Route
|
|
15
|
+
*/
|
|
16
|
+
export function createJsonStreamRoute(path, handler, options) {
|
|
17
|
+
const isNDJSON = options.mode === 'ndjson';
|
|
18
|
+
return defineRoute(options.method, path, async (ctx) => {
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
let controllerRef = null;
|
|
21
|
+
let closed = false;
|
|
22
|
+
let first = true;
|
|
23
|
+
const headers = {
|
|
24
|
+
'Content-Type': isNDJSON ? 'application/x-ndjson; charset=utf-8' : 'application/json; charset=utf-8',
|
|
25
|
+
...(isNDJSON ? { 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no' } : {}),
|
|
26
|
+
};
|
|
27
|
+
const enqueueArray = (chunk) => {
|
|
28
|
+
if (!controllerRef)
|
|
29
|
+
return;
|
|
30
|
+
if (first) {
|
|
31
|
+
controllerRef.enqueue(encoder.encode('[' + chunk));
|
|
32
|
+
first = false;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
controllerRef.enqueue(encoder.encode(',' + chunk));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const writer = {
|
|
39
|
+
send(value) {
|
|
40
|
+
if (closed || !controllerRef)
|
|
41
|
+
return;
|
|
42
|
+
const json = JSON.stringify(value);
|
|
43
|
+
if (isNDJSON) {
|
|
44
|
+
controllerRef.enqueue(encoder.encode(json + '\n'));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
enqueueArray(json);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
write(chunk) {
|
|
51
|
+
if (closed || !controllerRef)
|
|
52
|
+
return;
|
|
53
|
+
const encoded = typeof chunk === 'string' ? encoder.encode(chunk) : chunk;
|
|
54
|
+
controllerRef.enqueue(encoded);
|
|
55
|
+
},
|
|
56
|
+
close() {
|
|
57
|
+
if (closed)
|
|
58
|
+
return;
|
|
59
|
+
if (!isNDJSON && controllerRef && !first) {
|
|
60
|
+
controllerRef.enqueue(encoder.encode(']'));
|
|
61
|
+
}
|
|
62
|
+
closed = true;
|
|
63
|
+
controllerRef?.close();
|
|
64
|
+
},
|
|
65
|
+
setHeader(key, value) {
|
|
66
|
+
headers[key] = value;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const body = new ReadableStream({
|
|
70
|
+
start(controller) {
|
|
71
|
+
controllerRef = controller;
|
|
72
|
+
ctx.request.signal.addEventListener('abort', () => {
|
|
73
|
+
closed = true;
|
|
74
|
+
controllerRef?.close();
|
|
75
|
+
});
|
|
76
|
+
Promise.resolve(handler({ ...ctx, response: writer })).catch((err) => {
|
|
77
|
+
try {
|
|
78
|
+
controller.error(err);
|
|
79
|
+
}
|
|
80
|
+
catch { /* noop */ }
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
cancel() {
|
|
84
|
+
closed = true;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return new Response(body, {
|
|
88
|
+
status: 200,
|
|
89
|
+
headers,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -1,17 +1,79 @@
|
|
|
1
|
-
import { HeadersInit } from
|
|
1
|
+
import { BodyInit, HeadersInit } from 'undici';
|
|
2
|
+
/**
|
|
3
|
+
* A standardized shape for internal route result objects.
|
|
4
|
+
* These are later converted into native `Response` instances.
|
|
5
|
+
*/
|
|
2
6
|
export interface ResultResponse<T = unknown> {
|
|
7
|
+
/**
|
|
8
|
+
* Optional body content (can be a string, object, or binary).
|
|
9
|
+
*/
|
|
3
10
|
body?: T;
|
|
11
|
+
/**
|
|
12
|
+
* HTTP status code (e.g. 200, 404, 500).
|
|
13
|
+
*/
|
|
4
14
|
status: number;
|
|
15
|
+
/**
|
|
16
|
+
* Optional response headers.
|
|
17
|
+
*/
|
|
5
18
|
headers?: HeadersInit;
|
|
6
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* 200 OK
|
|
22
|
+
*/
|
|
7
23
|
export declare const ok: <T>(body: T, headers?: HeadersInit) => ResultResponse<T>;
|
|
24
|
+
/**
|
|
25
|
+
* 201 Created
|
|
26
|
+
*/
|
|
8
27
|
export declare const created: <T>(body: T, headers?: HeadersInit) => ResultResponse<T>;
|
|
28
|
+
/**
|
|
29
|
+
* 204 No Content
|
|
30
|
+
*/
|
|
9
31
|
export declare const noContent: (headers?: HeadersInit) => ResultResponse<undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* 304 Not Modified
|
|
34
|
+
*/
|
|
10
35
|
export declare const notModified: (headers?: HeadersInit) => ResultResponse<undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* 400 Bad Request
|
|
38
|
+
*/
|
|
11
39
|
export declare const badRequest: <T = string>(body?: T, headers?: HeadersInit) => ResultResponse<T>;
|
|
40
|
+
/**
|
|
41
|
+
* 401 Unauthorized
|
|
42
|
+
*/
|
|
12
43
|
export declare const unauthorized: <T = string>(body?: T, headers?: HeadersInit) => ResultResponse<T>;
|
|
44
|
+
/**
|
|
45
|
+
* 403 Forbidden
|
|
46
|
+
*/
|
|
13
47
|
export declare const forbidden: <T = string>(body?: T, headers?: HeadersInit) => ResultResponse<T>;
|
|
48
|
+
/**
|
|
49
|
+
* 404 Not Found
|
|
50
|
+
*/
|
|
14
51
|
export declare const notFound: <T = string>(body?: T, headers?: HeadersInit) => ResultResponse<T>;
|
|
52
|
+
/**
|
|
53
|
+
* 405 Method Not Allowed
|
|
54
|
+
*/
|
|
15
55
|
export declare const methodNotAllowed: <T = string>(body?: T, headers?: HeadersInit) => ResultResponse<T>;
|
|
56
|
+
/**
|
|
57
|
+
* 500 Internal Server Error
|
|
58
|
+
*/
|
|
16
59
|
export declare const internalError: (err: unknown, headers?: HeadersInit) => ResultResponse<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Sends a binary or stream-based file response with optional content-disposition.
|
|
62
|
+
*
|
|
63
|
+
* @param content - The file data (Blob, ArrayBuffer, or stream)
|
|
64
|
+
* @param contentType - MIME type of the file
|
|
65
|
+
* @param fileName - Optional download filename
|
|
66
|
+
* @param headers - Optional extra headers
|
|
67
|
+
* @returns A ResultResponse for download-ready content
|
|
68
|
+
*/
|
|
69
|
+
export declare const fileResponse: (content: Blob | ArrayBuffer | ReadableStream<Uint8Array>, contentType: string, fileName?: string, headers?: HeadersInit) => ResultResponse<BodyInit>;
|
|
70
|
+
/**
|
|
71
|
+
* Converts an internal `ResultResponse` into a native `Response` object
|
|
72
|
+
* for use inside Astro API routes.
|
|
73
|
+
*
|
|
74
|
+
* Automatically applies `Content-Type: application/json` for object bodies.
|
|
75
|
+
*
|
|
76
|
+
* @param result - A ResultResponse returned from route handler
|
|
77
|
+
* @returns A native Response
|
|
78
|
+
*/
|
|
17
79
|
export declare function toAstroResponse(result: ResultResponse | undefined): Response;
|