rouzer 5.2.1 → 5.3.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 +24 -1
- package/dist/http.d.ts +18 -11
- package/dist/http.js +12 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/metadata.d.ts +16 -0
- package/dist/metadata.js +14 -0
- package/dist/ndjson.d.ts +1 -1
- package/dist/ndjson.js +172 -99
- package/docs/context.md +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Import the primary API from the root package and declare routes through the HTTP
|
|
|
59
59
|
subpath:
|
|
60
60
|
|
|
61
61
|
```ts
|
|
62
|
-
import { $error, $type, chain, createClient, createRouter } from 'rouzer'
|
|
62
|
+
import { $error, $type, chain, createClient, createRouter, metadata } from 'rouzer'
|
|
63
63
|
import * as http from 'rouzer/http'
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -181,6 +181,29 @@ await client.upload(file, { headers: { 'content-type': file.type } })
|
|
|
181
181
|
Server handlers for raw-body routes read from `ctx.request` directly with Fetch
|
|
182
182
|
APIs such as `arrayBuffer()`, `blob()`, `formData()`, or `text()`.
|
|
183
183
|
|
|
184
|
+
### Route metadata
|
|
185
|
+
|
|
186
|
+
Use `metadata(...)` to attach optional runtime metadata to HTTP resources or
|
|
187
|
+
actions. Metadata does not affect routing, validation, client typing, or handler
|
|
188
|
+
behavior; it is preserved on route nodes for generated clients, CLIs, docs, and
|
|
189
|
+
route inspectors.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
export const sessions = http.resource('sessions', {
|
|
193
|
+
...metadata({
|
|
194
|
+
description: 'Daemon-managed session control.',
|
|
195
|
+
}),
|
|
196
|
+
list: http.post('list', {
|
|
197
|
+
...metadata({
|
|
198
|
+
description: 'Lists daemon-managed sessions and pagination state.',
|
|
199
|
+
}),
|
|
200
|
+
response: $type<SessionList>(),
|
|
201
|
+
}),
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The constructed nodes expose metadata as `node.metadata`.
|
|
206
|
+
|
|
184
207
|
### Client lifecycle hooks
|
|
185
208
|
|
|
186
209
|
Pass `clientHook` to observe generated client action calls without wrapping the
|
package/dist/http.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
+
import { type RouteMetadata, type RouteMetadataMarker } from './metadata.js';
|
|
2
3
|
import type { RawBodySchema, RouteSchema } from './types/schema.js';
|
|
3
4
|
/** HTTP methods supported by Rouzer action declarations. */
|
|
4
5
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
@@ -17,6 +18,8 @@ export type HttpAction<P extends string = string, T extends RouteSchema = RouteS
|
|
|
17
18
|
method: M;
|
|
18
19
|
/** Request validation and optional response type schema. */
|
|
19
20
|
schema: T;
|
|
21
|
+
/** Optional runtime metadata for generated tooling. */
|
|
22
|
+
metadata?: RouteMetadata;
|
|
20
23
|
};
|
|
21
24
|
/**
|
|
22
25
|
* Path-scoped namespace in an HTTP route tree.
|
|
@@ -31,6 +34,8 @@ export type HttpResource<P extends string = string, TChildren extends HttpRouteT
|
|
|
31
34
|
path: RoutePattern<P>;
|
|
32
35
|
/** Child resources and actions exposed below this resource. */
|
|
33
36
|
children: TChildren;
|
|
37
|
+
/** Optional runtime metadata for generated tooling. */
|
|
38
|
+
metadata?: RouteMetadata;
|
|
34
39
|
};
|
|
35
40
|
/** Node type accepted inside an HTTP route tree. */
|
|
36
41
|
export type HttpNode = HttpAction | HttpResource;
|
|
@@ -38,6 +43,8 @@ export type HttpNode = HttpAction | HttpResource;
|
|
|
38
43
|
export type HttpRouteTree = {
|
|
39
44
|
[key: string]: HttpNode;
|
|
40
45
|
};
|
|
46
|
+
type RouteDeclaration<T extends object> = T & Partial<RouteMetadataMarker>;
|
|
47
|
+
type StringKeys<T> = Pick<T, Extract<keyof T, string>>;
|
|
41
48
|
/**
|
|
42
49
|
* Declare an HTTP resource namespace.
|
|
43
50
|
*
|
|
@@ -45,22 +52,22 @@ export type HttpRouteTree = {
|
|
|
45
52
|
* property names are API names only; they do not affect the URL unless the child
|
|
46
53
|
* is another resource or an action with an explicit path.
|
|
47
54
|
*/
|
|
48
|
-
export declare function resource<const P extends string, const TChildren extends HttpRouteTree>(path: P, children: TChildren): HttpResource<P, TChildren
|
|
55
|
+
export declare function resource<const P extends string, const TChildren extends HttpRouteTree>(path: P, children: RouteDeclaration<TChildren>): HttpResource<P, StringKeys<TChildren>>;
|
|
49
56
|
/** Declare a GET action, optionally with an action-local path segment. */
|
|
50
|
-
export declare function get<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'GET'>;
|
|
51
|
-
export declare function get<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'GET'>;
|
|
57
|
+
export declare function get<const P extends string, const T extends RouteSchema>(path: P, schema: RouteDeclaration<T>): HttpAction<P, T, 'GET'>;
|
|
58
|
+
export declare function get<const T extends RouteSchema>(schema: RouteDeclaration<T>): HttpAction<'', T, 'GET'>;
|
|
52
59
|
/** Declare a POST action, optionally with an action-local path segment. */
|
|
53
|
-
export declare function post<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'POST'>;
|
|
54
|
-
export declare function post<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'POST'>;
|
|
60
|
+
export declare function post<const P extends string, const T extends RouteSchema>(path: P, schema: RouteDeclaration<T>): HttpAction<P, T, 'POST'>;
|
|
61
|
+
export declare function post<const T extends RouteSchema>(schema: RouteDeclaration<T>): HttpAction<'', T, 'POST'>;
|
|
55
62
|
/** Declare a PUT action, optionally with an action-local path segment. */
|
|
56
|
-
export declare function put<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'PUT'>;
|
|
57
|
-
export declare function put<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'PUT'>;
|
|
63
|
+
export declare function put<const P extends string, const T extends RouteSchema>(path: P, schema: RouteDeclaration<T>): HttpAction<P, T, 'PUT'>;
|
|
64
|
+
export declare function put<const T extends RouteSchema>(schema: RouteDeclaration<T>): HttpAction<'', T, 'PUT'>;
|
|
58
65
|
/** Declare a PATCH action, optionally with an action-local path segment. */
|
|
59
|
-
export declare function patch<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'PATCH'>;
|
|
60
|
-
export declare function patch<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'PATCH'>;
|
|
66
|
+
export declare function patch<const P extends string, const T extends RouteSchema>(path: P, schema: RouteDeclaration<T>): HttpAction<P, T, 'PATCH'>;
|
|
67
|
+
export declare function patch<const T extends RouteSchema>(schema: RouteDeclaration<T>): HttpAction<'', T, 'PATCH'>;
|
|
61
68
|
/** Declare a DELETE action, optionally with an action-local path segment. */
|
|
62
|
-
declare function deleteAction<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'DELETE'>;
|
|
63
|
-
declare function deleteAction<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'DELETE'>;
|
|
69
|
+
declare function deleteAction<const P extends string, const T extends RouteSchema>(path: P, schema: RouteDeclaration<T>): HttpAction<P, T, 'DELETE'>;
|
|
70
|
+
declare function deleteAction<const T extends RouteSchema>(schema: RouteDeclaration<T>): HttpAction<'', T, 'DELETE'>;
|
|
64
71
|
export { deleteAction as delete };
|
|
65
72
|
/**
|
|
66
73
|
* Declare a request body that is passed through to `fetch` without JSON encoding.
|
package/dist/http.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
+
import { getRouteMetadata, stripRouteMetadata, } from './metadata.js';
|
|
2
3
|
/**
|
|
3
4
|
* Declare an HTTP resource namespace.
|
|
4
5
|
*
|
|
@@ -7,10 +8,12 @@ import { RoutePattern } from '@remix-run/route-pattern';
|
|
|
7
8
|
* is another resource or an action with an explicit path.
|
|
8
9
|
*/
|
|
9
10
|
export function resource(path, children) {
|
|
11
|
+
const metadata = getRouteMetadata(children);
|
|
10
12
|
return {
|
|
11
13
|
kind: 'resource',
|
|
12
14
|
path: RoutePattern.parse(path),
|
|
13
|
-
children,
|
|
15
|
+
children: stripRouteMetadata(children),
|
|
16
|
+
metadata,
|
|
14
17
|
};
|
|
15
18
|
}
|
|
16
19
|
export function get(pathOrSchema, schema) {
|
|
@@ -47,5 +50,12 @@ function action(method, pathOrSchema, schema) {
|
|
|
47
50
|
? RoutePattern.parse(pathOrSchema)
|
|
48
51
|
: undefined;
|
|
49
52
|
schema ??= typeof pathOrSchema === 'string' ? {} : pathOrSchema;
|
|
50
|
-
|
|
53
|
+
const metadata = getRouteMetadata(schema);
|
|
54
|
+
return {
|
|
55
|
+
kind: 'action',
|
|
56
|
+
path,
|
|
57
|
+
method,
|
|
58
|
+
schema: stripRouteMetadata(schema),
|
|
59
|
+
metadata,
|
|
60
|
+
};
|
|
51
61
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare const routeMetadataKey: unique symbol;
|
|
2
|
+
/** Runtime metadata attached to Rouzer route nodes. */
|
|
3
|
+
export type RouteMetadata = {
|
|
4
|
+
/** Short label for generated indexes, clients, CLIs, or docs. */
|
|
5
|
+
summary?: string;
|
|
6
|
+
/** Human-readable route description for generated tooling. */
|
|
7
|
+
description?: string;
|
|
8
|
+
};
|
|
9
|
+
export type RouteMetadataMarker = {
|
|
10
|
+
readonly [routeMetadataKey]: RouteMetadata;
|
|
11
|
+
};
|
|
12
|
+
/** Attach runtime metadata to a route declaration. */
|
|
13
|
+
export declare function metadata(value: RouteMetadata): RouteMetadataMarker;
|
|
14
|
+
export declare function getRouteMetadata(value: unknown): RouteMetadata | undefined;
|
|
15
|
+
export declare function stripRouteMetadata<T extends object>(value: T): Omit<T, typeof routeMetadataKey>;
|
|
16
|
+
export {};
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const routeMetadataKey = Symbol('rouzer.metadata');
|
|
2
|
+
/** Attach runtime metadata to a route declaration. */
|
|
3
|
+
export function metadata(value) {
|
|
4
|
+
return { [routeMetadataKey]: value };
|
|
5
|
+
}
|
|
6
|
+
export function getRouteMetadata(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null
|
|
8
|
+
? value[routeMetadataKey]
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
11
|
+
export function stripRouteMetadata(value) {
|
|
12
|
+
const { [routeMetadataKey]: _metadata, ...rest } = value;
|
|
13
|
+
return rest;
|
|
14
|
+
}
|
package/dist/ndjson.d.ts
CHANGED
|
@@ -57,5 +57,5 @@ export declare function decodeNdjson<T = unknown>(stream: ReadableStream<Uint8Ar
|
|
|
57
57
|
* `content-type: application/x-ndjson; charset=utf-8` unless the caller supplies
|
|
58
58
|
* a content type in `init.headers`.
|
|
59
59
|
*/
|
|
60
|
-
export declare function ndjsonResponse<T>(source: NdjsonSource<T>, init?: ResponseInit & NdjsonEncodeOptions): Response;
|
|
60
|
+
export declare function ndjsonResponse<T>(source: NdjsonSource<T>, { signal, ...init }?: ResponseInit & NdjsonEncodeOptions): Response;
|
|
61
61
|
export {};
|
package/dist/ndjson.js
CHANGED
|
@@ -52,107 +52,184 @@ export const routerPlugin = {
|
|
|
52
52
|
* closed.
|
|
53
53
|
*/
|
|
54
54
|
export function encodeNdjson(source, options = {}) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
return new ReadableStream(new NdjsonEncoder(source, options));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Decode a newline-delimited JSON byte stream.
|
|
59
|
+
*
|
|
60
|
+
* @remarks UTF-8 chunks may split JSON lines. Both `\n` and `\r\n` line endings
|
|
61
|
+
* are accepted, and a final line does not need a trailing newline. Malformed
|
|
62
|
+
* lines throw a `SyntaxError` that includes the 1-based line number.
|
|
63
|
+
*/
|
|
64
|
+
export function decodeNdjson(stream) {
|
|
65
|
+
return new NdjsonDecoder(stream);
|
|
66
|
+
}
|
|
67
|
+
class NdjsonEncoder {
|
|
68
|
+
options;
|
|
69
|
+
iterator;
|
|
70
|
+
encoder = new TextEncoder();
|
|
71
|
+
cancelled = false;
|
|
72
|
+
cleanup;
|
|
73
|
+
abortHandler;
|
|
74
|
+
constructor(source, options) {
|
|
75
|
+
this.options = options;
|
|
76
|
+
this.iterator = getAsyncIterator(source);
|
|
72
77
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
abortHandler = () => {
|
|
79
|
-
void cancelIterator(signal.reason).catch(() => { });
|
|
78
|
+
start(controller) {
|
|
79
|
+
const { signal } = this.options;
|
|
80
|
+
if (signal) {
|
|
81
|
+
this.abortHandler = () => {
|
|
82
|
+
void this.cancel(signal.reason).catch(() => { });
|
|
80
83
|
try {
|
|
81
84
|
controller.close();
|
|
82
85
|
}
|
|
83
86
|
catch { }
|
|
84
87
|
};
|
|
85
88
|
if (signal.aborted) {
|
|
86
|
-
abortHandler();
|
|
87
|
-
return;
|
|
89
|
+
this.abortHandler();
|
|
88
90
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
async pull(controller) {
|
|
92
|
-
if (cancelled) {
|
|
93
|
-
controller.close();
|
|
94
|
-
return;
|
|
91
|
+
else {
|
|
92
|
+
signal.addEventListener('abort', this.abortHandler, { once: true });
|
|
95
93
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async pull(controller) {
|
|
97
|
+
if (this.cancelled) {
|
|
98
|
+
controller.close();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const { done, value } = await this.iterator.next();
|
|
102
|
+
if (this.cancelled) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (done) {
|
|
106
|
+
this.removeAbortHandler();
|
|
107
|
+
controller.close();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const line = JSON.stringify(value);
|
|
111
|
+
if (line === undefined) {
|
|
112
|
+
throw new TypeError('NDJSON items must serialize to a JSON text; received undefined');
|
|
113
|
+
}
|
|
114
|
+
controller.enqueue(this.encoder.encode(`${line}\n`));
|
|
115
|
+
}
|
|
116
|
+
async cancel(reason) {
|
|
117
|
+
if (!this.cancelled) {
|
|
118
|
+
this.cancelled = true;
|
|
119
|
+
this.removeAbortHandler();
|
|
120
|
+
this.cleanup ??= Promise.resolve(this.iterator.return?.(reason)).then(() => { });
|
|
121
|
+
}
|
|
122
|
+
await this.cleanup;
|
|
123
|
+
}
|
|
124
|
+
removeAbortHandler() {
|
|
125
|
+
const { signal } = this.options;
|
|
126
|
+
if (signal && this.abortHandler) {
|
|
127
|
+
signal.removeEventListener('abort', this.abortHandler);
|
|
128
|
+
this.abortHandler = undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
class NdjsonDecoder {
|
|
133
|
+
reader;
|
|
134
|
+
decoder = new TextDecoder();
|
|
135
|
+
buffer = '';
|
|
136
|
+
lineNumber = 0;
|
|
137
|
+
closed = false;
|
|
138
|
+
doneReading = false;
|
|
139
|
+
readerReleased = false;
|
|
140
|
+
constructor(stream) {
|
|
141
|
+
this.reader = stream.getReader();
|
|
142
|
+
}
|
|
143
|
+
[Symbol.asyncIterator]() {
|
|
144
|
+
return new NdjsonAsyncIterator(this);
|
|
145
|
+
}
|
|
146
|
+
releaseReader() {
|
|
147
|
+
if (!this.readerReleased) {
|
|
148
|
+
this.readerReleased = true;
|
|
149
|
+
this.reader.releaseLock();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async cancelReader(reason) {
|
|
153
|
+
if (!this.doneReading) {
|
|
154
|
+
await this.reader.cancel(reason).catch(() => { });
|
|
155
|
+
}
|
|
156
|
+
this.releaseReader();
|
|
157
|
+
}
|
|
158
|
+
async parseNextLine(line) {
|
|
159
|
+
try {
|
|
160
|
+
this.lineNumber += 1;
|
|
161
|
+
return {
|
|
162
|
+
done: false,
|
|
163
|
+
value: parseNdjsonLine(stripCarriageReturn(line), this.lineNumber),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
this.closed = true;
|
|
168
|
+
await this.cancelReader(error);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
class NdjsonAsyncIterator {
|
|
174
|
+
decoder;
|
|
175
|
+
closed = false;
|
|
176
|
+
constructor(decoder) {
|
|
177
|
+
this.decoder = decoder;
|
|
178
|
+
}
|
|
179
|
+
async next() {
|
|
180
|
+
if (this.closed || this.decoder.closed) {
|
|
181
|
+
return { done: true, value: undefined };
|
|
182
|
+
}
|
|
183
|
+
while (true) {
|
|
184
|
+
const newlineIndex = this.decoder.buffer.indexOf('\n');
|
|
185
|
+
if (newlineIndex !== -1) {
|
|
186
|
+
const line = this.decoder.buffer.slice(0, newlineIndex);
|
|
187
|
+
this.decoder.buffer = this.decoder.buffer.slice(newlineIndex + 1);
|
|
188
|
+
return this.decoder.parseNextLine(line);
|
|
99
189
|
}
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
190
|
+
if (this.decoder.doneReading) {
|
|
191
|
+
this.close();
|
|
192
|
+
this.decoder.releaseReader();
|
|
193
|
+
if (this.decoder.buffer.length > 0) {
|
|
194
|
+
const line = this.decoder.buffer;
|
|
195
|
+
this.decoder.buffer = '';
|
|
196
|
+
return this.decoder.parseNextLine(line);
|
|
197
|
+
}
|
|
198
|
+
return { done: true, value: undefined };
|
|
104
199
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
200
|
+
let chunk;
|
|
201
|
+
try {
|
|
202
|
+
chunk = await this.decoder.reader.read();
|
|
108
203
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Decode a newline-delimited JSON byte stream.
|
|
118
|
-
*
|
|
119
|
-
* @remarks UTF-8 chunks may split JSON lines. Both `\n` and `\r\n` line endings
|
|
120
|
-
* are accepted, and a final line does not need a trailing newline. Malformed
|
|
121
|
-
* lines throw a `SyntaxError` that includes the 1-based line number.
|
|
122
|
-
*/
|
|
123
|
-
export async function* decodeNdjson(stream) {
|
|
124
|
-
const reader = stream.getReader();
|
|
125
|
-
const decoder = new TextDecoder();
|
|
126
|
-
let buffer = '';
|
|
127
|
-
let lineNumber = 0;
|
|
128
|
-
let doneReading = false;
|
|
129
|
-
try {
|
|
130
|
-
while (true) {
|
|
131
|
-
const { done, value } = await reader.read();
|
|
132
|
-
if (done) {
|
|
133
|
-
buffer += decoder.decode();
|
|
134
|
-
doneReading = true;
|
|
135
|
-
break;
|
|
204
|
+
catch (error) {
|
|
205
|
+
this.close();
|
|
206
|
+
this.decoder.releaseReader();
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
if (this.closed || this.decoder.closed) {
|
|
210
|
+
return { done: true, value: undefined };
|
|
136
211
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
212
|
+
if (chunk.done) {
|
|
213
|
+
this.decoder.buffer += this.decoder.decoder.decode();
|
|
214
|
+
this.decoder.doneReading = true;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
this.decoder.buffer += this.decoder.decoder.decode(chunk.value, {
|
|
218
|
+
stream: true,
|
|
219
|
+
});
|
|
144
220
|
}
|
|
145
|
-
}
|
|
146
|
-
if (buffer.length > 0) {
|
|
147
|
-
lineNumber += 1;
|
|
148
|
-
yield parseNdjsonLine(stripCarriageReturn(buffer), lineNumber);
|
|
149
221
|
}
|
|
150
222
|
}
|
|
151
|
-
|
|
152
|
-
if (!
|
|
153
|
-
|
|
223
|
+
async return(reason) {
|
|
224
|
+
if (!this.closed) {
|
|
225
|
+
this.close();
|
|
226
|
+
await this.decoder.cancelReader(reason);
|
|
154
227
|
}
|
|
155
|
-
|
|
228
|
+
return { done: true, value: undefined };
|
|
229
|
+
}
|
|
230
|
+
close() {
|
|
231
|
+
this.closed = true;
|
|
232
|
+
this.decoder.closed = true;
|
|
156
233
|
}
|
|
157
234
|
}
|
|
158
235
|
/**
|
|
@@ -162,32 +239,28 @@ export async function* decodeNdjson(stream) {
|
|
|
162
239
|
* `content-type: application/x-ndjson; charset=utf-8` unless the caller supplies
|
|
163
240
|
* a content type in `init.headers`.
|
|
164
241
|
*/
|
|
165
|
-
export function ndjsonResponse(source, init = {}) {
|
|
166
|
-
const { signal, ...responseInit } = init;
|
|
242
|
+
export function ndjsonResponse(source, { signal, ...init } = {}) {
|
|
167
243
|
const headers = new Headers(init.headers);
|
|
168
244
|
if (!headers.has('content-type')) {
|
|
169
245
|
headers.set('content-type', 'application/x-ndjson; charset=utf-8');
|
|
170
246
|
}
|
|
171
247
|
return new Response(encodeNdjson(source, { signal }), {
|
|
172
|
-
...
|
|
248
|
+
...init,
|
|
173
249
|
headers,
|
|
174
250
|
});
|
|
175
251
|
}
|
|
176
252
|
function getAsyncIterator(source) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return asyncIterator;
|
|
253
|
+
if (Symbol.asyncIterator in source) {
|
|
254
|
+
return source[Symbol.asyncIterator]();
|
|
180
255
|
}
|
|
181
|
-
|
|
182
|
-
|
|
256
|
+
if (Symbol.iterator in source) {
|
|
257
|
+
const iterator = source[Symbol.iterator]();
|
|
183
258
|
return {
|
|
184
|
-
next()
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return { done: true, value: undefined };
|
|
190
|
-
},
|
|
259
|
+
next: async (value) => iterator.next(value),
|
|
260
|
+
return: iterator.return
|
|
261
|
+
? async (value) => iterator.return(value)
|
|
262
|
+
: undefined,
|
|
263
|
+
throw: iterator.throw ? async (error) => iterator.throw(error) : undefined,
|
|
191
264
|
};
|
|
192
265
|
}
|
|
193
266
|
throw new TypeError('NDJSON source must be iterable');
|
package/docs/context.md
CHANGED
|
@@ -72,6 +72,29 @@ paths are joined, so the examples above expose `profiles/:id`, `profiles/:id`,
|
|
|
72
72
|
and `profiles/:id/posts`. Path params from parent resources are accumulated into
|
|
73
73
|
child action types.
|
|
74
74
|
|
|
75
|
+
Use the root `metadata(...)` helper to attach optional runtime metadata to
|
|
76
|
+
resources or actions without changing route matching, validation, client typing,
|
|
77
|
+
or handler behavior:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { metadata } from 'rouzer'
|
|
81
|
+
|
|
82
|
+
export const sessions = http.resource('sessions', {
|
|
83
|
+
...metadata({
|
|
84
|
+
description: 'Daemon-managed session control.',
|
|
85
|
+
}),
|
|
86
|
+
list: http.post('list', {
|
|
87
|
+
...metadata({
|
|
88
|
+
description: 'Lists daemon-managed sessions and pagination state.',
|
|
89
|
+
}),
|
|
90
|
+
response: $type<SessionList>(),
|
|
91
|
+
}),
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Constructed route nodes expose metadata through `node.metadata` for generated
|
|
96
|
+
clients, CLIs, docs, and route inspectors.
|
|
97
|
+
|
|
75
98
|
Patterns are parsed by `@remix-run/route-pattern` v0.21. Params can be inferred
|
|
76
99
|
from patterns such as `hello/:name`, `v:major.:minor`,
|
|
77
100
|
`api(/v:major(.:minor))`, `assets/*path`, and `search?q`. Full URL patterns such
|