astro-routify 1.1.0 → 1.2.1

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.
@@ -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,18 +1,79 @@
1
- import { BodyInit, HeadersInit } from "undici";
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
+ */
17
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
+ */
18
79
  export declare function toAstroResponse(result: ResultResponse | undefined): Response;
@@ -1,16 +1,61 @@
1
+ /**
2
+ * Internal helper to build a typed `ResultResponse`.
3
+ * @param status - HTTP status code
4
+ * @param body - Optional response body
5
+ * @param headers - Optional headers
6
+ */
1
7
  function createResponse(status, body, headers) {
2
8
  return { status, body, headers };
3
9
  }
10
+ /**
11
+ * 200 OK
12
+ */
4
13
  export const ok = (body, headers) => createResponse(200, body, headers);
14
+ /**
15
+ * 201 Created
16
+ */
5
17
  export const created = (body, headers) => createResponse(201, body, headers);
18
+ /**
19
+ * 204 No Content
20
+ */
6
21
  export const noContent = (headers) => createResponse(204, undefined, headers);
22
+ /**
23
+ * 304 Not Modified
24
+ */
7
25
  export const notModified = (headers) => createResponse(304, undefined, headers);
26
+ /**
27
+ * 400 Bad Request
28
+ */
8
29
  export const badRequest = (body = 'Bad Request', headers) => createResponse(400, body, headers);
30
+ /**
31
+ * 401 Unauthorized
32
+ */
9
33
  export const unauthorized = (body = 'Unauthorized', headers) => createResponse(401, body, headers);
34
+ /**
35
+ * 403 Forbidden
36
+ */
10
37
  export const forbidden = (body = 'Forbidden', headers) => createResponse(403, body, headers);
38
+ /**
39
+ * 404 Not Found
40
+ */
11
41
  export const notFound = (body = 'Not Found', headers) => createResponse(404, body, headers);
42
+ /**
43
+ * 405 Method Not Allowed
44
+ */
12
45
  export const methodNotAllowed = (body = 'Method Not Allowed', headers) => createResponse(405, body, headers);
46
+ /**
47
+ * 500 Internal Server Error
48
+ */
13
49
  export const internalError = (err, headers) => createResponse(500, err instanceof Error ? err.message : String(err), headers);
50
+ /**
51
+ * Sends a binary or stream-based file response with optional content-disposition.
52
+ *
53
+ * @param content - The file data (Blob, ArrayBuffer, or stream)
54
+ * @param contentType - MIME type of the file
55
+ * @param fileName - Optional download filename
56
+ * @param headers - Optional extra headers
57
+ * @returns A ResultResponse for download-ready content
58
+ */
14
59
  export const fileResponse = (content, contentType, fileName, headers) => {
15
60
  const disposition = fileName
16
61
  ? { 'Content-Disposition': `attachment; filename="${fileName}"` }
@@ -25,6 +70,15 @@ export const fileResponse = (content, contentType, fileName, headers) => {
25
70
  },
26
71
  };
27
72
  };
73
+ /**
74
+ * Converts an internal `ResultResponse` into a native `Response` object
75
+ * for use inside Astro API routes.
76
+ *
77
+ * Automatically applies `Content-Type: application/json` for object bodies.
78
+ *
79
+ * @param result - A ResultResponse returned from route handler
80
+ * @returns A native Response
81
+ */
28
82
  export function toAstroResponse(result) {
29
83
  if (!result)
30
84
  return new Response(null, { status: 204 });
@@ -0,0 +1,75 @@
1
+ import type { APIContext } from 'astro';
2
+ import { type Route } from './defineRoute';
3
+ import { HttpMethod } from './HttpMethod';
4
+ import { HeadersInit } from 'undici';
5
+ /**
6
+ * A writer for streaming raw data to the response body.
7
+ *
8
+ * This is used inside the `stream()` route handler to emit bytes
9
+ * or strings directly to the client with backpressure awareness.
10
+ */
11
+ export interface StreamWriter {
12
+ /**
13
+ * Write a string or Uint8Array chunk to the response stream.
14
+ * @param chunk - Text or byte chunk to emit
15
+ */
16
+ write: (chunk: string | Uint8Array) => void;
17
+ /**
18
+ * Close the stream and signal end-of-response to the client.
19
+ */
20
+ close: () => void;
21
+ /**
22
+ * Dynamically change the Content-Type header.
23
+ * Should be called before the first `write()` to take effect.
24
+ * @param type - MIME type (e.g., `text/html`, `application/json`)
25
+ */
26
+ setContentType: (type: string) => void;
27
+ }
28
+ /**
29
+ * Configuration options for the `stream()` route helper.
30
+ */
31
+ export interface StreamOptions {
32
+ /**
33
+ * HTTP method (defaults to GET).
34
+ */
35
+ method?: HttpMethod;
36
+ /**
37
+ * Content-Type header (defaults to `text/event-stream`).
38
+ */
39
+ contentType?: string;
40
+ /**
41
+ * Additional custom headers.
42
+ */
43
+ headers?: HeadersInit;
44
+ /**
45
+ * Enable SSE keep-alive headers (defaults to true for SSE).
46
+ */
47
+ keepAlive?: boolean;
48
+ }
49
+ /**
50
+ * Defines a generic streaming route that can write raw chunks of data
51
+ * to the response in real time using a `ReadableStream`.
52
+ *
53
+ * Suitable for Server-Sent Events (SSE), long-polling, streamed HTML,
54
+ * logs, LLM output, or NDJSON responses.
55
+ *
56
+ * @example
57
+ * stream('/clock', async ({ response }) => {
58
+ * const timer = setInterval(() => {
59
+ * response.write(`data: ${new Date().toISOString()}\n\n`);
60
+ * }, 1000);
61
+ *
62
+ * setTimeout(() => {
63
+ * clearInterval(timer);
64
+ * response.close();
65
+ * }, 5000);
66
+ * });
67
+ *
68
+ * @param path - The route path (e.g. `/clock`)
69
+ * @param handler - Handler that receives the API context and response writer
70
+ * @param options - Optional stream configuration
71
+ * @returns A Route object ready to be used in `defineRouter()`
72
+ */
73
+ export declare function stream(path: string, handler: (ctx: APIContext & {
74
+ response: StreamWriter;
75
+ }) => void | Promise<void>, options?: StreamOptions): Route;
@@ -0,0 +1,93 @@
1
+ import { defineRoute } from './defineRoute';
2
+ import { HttpMethod } from './HttpMethod';
3
+ /**
4
+ * Defines a generic streaming route that can write raw chunks of data
5
+ * to the response in real time using a `ReadableStream`.
6
+ *
7
+ * Suitable for Server-Sent Events (SSE), long-polling, streamed HTML,
8
+ * logs, LLM output, or NDJSON responses.
9
+ *
10
+ * @example
11
+ * stream('/clock', async ({ response }) => {
12
+ * const timer = setInterval(() => {
13
+ * response.write(`data: ${new Date().toISOString()}\n\n`);
14
+ * }, 1000);
15
+ *
16
+ * setTimeout(() => {
17
+ * clearInterval(timer);
18
+ * response.close();
19
+ * }, 5000);
20
+ * });
21
+ *
22
+ * @param path - The route path (e.g. `/clock`)
23
+ * @param handler - Handler that receives the API context and response writer
24
+ * @param options - Optional stream configuration
25
+ * @returns A Route object ready to be used in `defineRouter()`
26
+ */
27
+ export function stream(path, handler, options) {
28
+ const method = options?.method ?? HttpMethod.GET;
29
+ return defineRoute(method, path, async (ctx) => {
30
+ let contentType = options?.contentType ?? 'text/event-stream';
31
+ const encoder = new TextEncoder();
32
+ let controllerRef = null;
33
+ let closed = false;
34
+ const writer = {
35
+ write: (chunk) => {
36
+ if (closed || !controllerRef)
37
+ return;
38
+ const bytes = typeof chunk === 'string' ?
39
+ encoder.encode(`data: ${chunk}\n\n`)
40
+ : chunk;
41
+ controllerRef.enqueue(bytes);
42
+ },
43
+ close: () => {
44
+ if (closed)
45
+ return;
46
+ closed = true;
47
+ try {
48
+ controllerRef?.close();
49
+ }
50
+ catch { /* noop */ }
51
+ },
52
+ setContentType: (type) => {
53
+ contentType = type;
54
+ },
55
+ };
56
+ const body = new ReadableStream({
57
+ start(controller) {
58
+ controllerRef = controller;
59
+ Promise.resolve(handler({ ...ctx, response: writer }))
60
+ .catch((err) => {
61
+ try {
62
+ controller.error(err);
63
+ }
64
+ catch { /* noop */ }
65
+ });
66
+ ctx.request.signal.addEventListener('abort', () => {
67
+ closed = true;
68
+ controller.close();
69
+ console.debug('Request aborted — streaming stopped.');
70
+ });
71
+ },
72
+ cancel() {
73
+ closed = true;
74
+ console.debug('Stream cancelled explicitly.');
75
+ },
76
+ });
77
+ const defaultSseHeaders = contentType === 'text/event-stream' && options?.keepAlive !== false
78
+ ? {
79
+ 'Cache-Control': 'no-cache',
80
+ 'Connection': 'keep-alive',
81
+ 'X-Accel-Buffering': 'no',
82
+ }
83
+ : {};
84
+ return new Response(body, {
85
+ status: 200,
86
+ headers: {
87
+ 'Content-Type': contentType,
88
+ ...defaultSseHeaders,
89
+ ...(options?.headers ?? {}),
90
+ },
91
+ });
92
+ });
93
+ }
@@ -0,0 +1,31 @@
1
+ import type { APIContext } from 'astro';
2
+ import { HttpMethod } from './HttpMethod';
3
+ import { Route } from './defineRoute';
4
+ import { JsonStreamWriter } from './internal/createJsonStreamRoute';
5
+ /**
6
+ * Defines a JSON streaming route that emits a valid JSON array.
7
+ *
8
+ * This helper returns a valid `application/json` response containing
9
+ * a streamable array of JSON values. Useful for large data exports
10
+ * or APIs where the full array can be streamed as it's generated.
11
+ *
12
+ * Unlike `streamJsonND()`, this wraps all values in `[` and `]`
13
+ * and separates them with commas.
14
+ *
15
+ * @example
16
+ * streamJsonArray('/users', async ({ response }) => {
17
+ * response.send({ id: 1 });
18
+ * response.send({ id: 2 });
19
+ * response.close();
20
+ * });
21
+ *
22
+ * @param path - The route path (e.g. `/users`)
23
+ * @param handler - A function that receives `ctx` and a JSON stream writer
24
+ * @param options - Optional HTTP method override (default is GET)
25
+ * @returns A Route object ready to be registered with `defineRouter`
26
+ */
27
+ export declare function streamJsonArray(path: string, handler: (ctx: APIContext & {
28
+ response: JsonStreamWriter;
29
+ }) => void | Promise<void>, options?: {
30
+ method?: HttpMethod;
31
+ }): Route;
@@ -0,0 +1,30 @@
1
+ import { HttpMethod } from './HttpMethod';
2
+ import { createJsonStreamRoute } from './internal/createJsonStreamRoute';
3
+ /**
4
+ * Defines a JSON streaming route that emits a valid JSON array.
5
+ *
6
+ * This helper returns a valid `application/json` response containing
7
+ * a streamable array of JSON values. Useful for large data exports
8
+ * or APIs where the full array can be streamed as it's generated.
9
+ *
10
+ * Unlike `streamJsonND()`, this wraps all values in `[` and `]`
11
+ * and separates them with commas.
12
+ *
13
+ * @example
14
+ * streamJsonArray('/users', async ({ response }) => {
15
+ * response.send({ id: 1 });
16
+ * response.send({ id: 2 });
17
+ * response.close();
18
+ * });
19
+ *
20
+ * @param path - The route path (e.g. `/users`)
21
+ * @param handler - A function that receives `ctx` and a JSON stream writer
22
+ * @param options - Optional HTTP method override (default is GET)
23
+ * @returns A Route object ready to be registered with `defineRouter`
24
+ */
25
+ export function streamJsonArray(path, handler, options) {
26
+ return createJsonStreamRoute(path, handler, {
27
+ mode: 'array',
28
+ method: options?.method ?? HttpMethod.GET,
29
+ });
30
+ }
@@ -0,0 +1,34 @@
1
+ import type { APIContext } from 'astro';
2
+ import { HttpMethod } from './HttpMethod';
3
+ import { Route } from './defineRoute';
4
+ import { JsonStreamWriter } from './internal/createJsonStreamRoute';
5
+ /**
6
+ * Defines a JSON streaming route using NDJSON (Newline Delimited JSON) format.
7
+ *
8
+ * This helper streams individual JSON objects separated by newlines (`\n`)
9
+ * instead of a single JSON array. It sets the `Content-Type` to
10
+ * `application/x-ndjson` and disables buffering to allow low-latency
11
+ * delivery of each item.
12
+ *
13
+ * Ideal for real-time updates, event logs, progressive loading, or
14
+ * integrations with LLMs or dashboards where each object can be processed
15
+ * as soon as it's received.
16
+ *
17
+ * @example
18
+ * streamJsonND('/events', async ({ response }) => {
19
+ * response.send({ type: 'start' });
20
+ * await delay(100);
21
+ * response.send({ type: 'progress', percent: 25 });
22
+ * response.close();
23
+ * });
24
+ *
25
+ * @param path - The route path (e.g. `/events`)
26
+ * @param handler - A function that receives `ctx` and a JSON stream writer
27
+ * @param options - Optional HTTP method override (default is GET)
28
+ * @returns A Route object ready to be registered with `defineRouter`
29
+ */
30
+ export declare function streamJsonND(path: string, handler: (ctx: APIContext & {
31
+ response: JsonStreamWriter;
32
+ }) => void | Promise<void>, options?: {
33
+ method?: HttpMethod;
34
+ }): Route;
@@ -0,0 +1,33 @@
1
+ import { HttpMethod } from './HttpMethod';
2
+ import { createJsonStreamRoute } from './internal/createJsonStreamRoute';
3
+ /**
4
+ * Defines a JSON streaming route using NDJSON (Newline Delimited JSON) format.
5
+ *
6
+ * This helper streams individual JSON objects separated by newlines (`\n`)
7
+ * instead of a single JSON array. It sets the `Content-Type` to
8
+ * `application/x-ndjson` and disables buffering to allow low-latency
9
+ * delivery of each item.
10
+ *
11
+ * Ideal for real-time updates, event logs, progressive loading, or
12
+ * integrations with LLMs or dashboards where each object can be processed
13
+ * as soon as it's received.
14
+ *
15
+ * @example
16
+ * streamJsonND('/events', async ({ response }) => {
17
+ * response.send({ type: 'start' });
18
+ * await delay(100);
19
+ * response.send({ type: 'progress', percent: 25 });
20
+ * response.close();
21
+ * });
22
+ *
23
+ * @param path - The route path (e.g. `/events`)
24
+ * @param handler - A function that receives `ctx` and a JSON stream writer
25
+ * @param options - Optional HTTP method override (default is GET)
26
+ * @returns A Route object ready to be registered with `defineRouter`
27
+ */
28
+ export function streamJsonND(path, handler, options) {
29
+ return createJsonStreamRoute(path, handler, {
30
+ mode: 'ndjson',
31
+ method: options?.method ?? HttpMethod.GET,
32
+ });
33
+ }