@rsc-kit/core 0.24.3 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/events.d.ts CHANGED
@@ -27,11 +27,11 @@ export interface EventsInput<P = Record<string, string>> {
27
27
  request: Request;
28
28
  }
29
29
  /** A message with a name or an id, for a client that listens by name or resumes. Made by `named()`. */
30
- export interface NamedEvent {
30
+ export interface NamedEvent<E extends string = string, D = unknown> {
31
31
  readonly [NAMED]: true;
32
- event?: string;
32
+ event?: E;
33
33
  id?: string;
34
- data: unknown;
34
+ data: D;
35
35
  }
36
36
  declare const NAMED: unique symbol;
37
37
  /**
@@ -41,17 +41,33 @@ declare const NAMED: unique symbol;
41
41
  * have a `data` key is still data, and the generator's own type stays the
42
42
  * type of what it yields.
43
43
  */
44
- export declare function named<D>(event: string, data: D, options?: {
44
+ export declare function named<const E extends string, D>(event: E, data: D, options?: {
45
45
  id?: string;
46
- }): NamedEvent;
46
+ }): NamedEvent<E, D>;
47
47
  /** How often a comment goes out on an idle stream, so a proxy keeps it open. */
48
48
  export declare const KEEPALIVE_MS = 15000;
49
49
  /** One message, framed. */
50
50
  export declare function frame(value: unknown): string;
51
- export declare function events<P = Record<string, string>, T = unknown>(produce: (input: EventsInput<P>) => AsyncIterable<T | NamedEvent>, options?: {
52
- keepaliveMs?: number;
53
- }): (request: Request, input: {
51
+ /**
52
+ * What `events()` returns: a GET handler that also says, as a type only, what
53
+ * its generator yields. Nothing reads `~events` at runtime; the build records
54
+ * it per url, which is how `useEvents('/api/orders/42/events')` types a
55
+ * message without being told - and stops compiling when the route changes
56
+ * what it sends.
57
+ */
58
+ export type EventsRoute<P = Record<string, string>, Y = unknown> = ((request: Request, input: {
54
59
  params: Promise<P>;
55
60
  searchParams: Promise<URLSearchParams>;
56
- }) => Response;
61
+ }) => Response) & {
62
+ readonly "~events"?: {
63
+ yields: Y;
64
+ };
65
+ };
66
+ /** The unnamed messages among a stream's yields: what `useEvents` receives without `event`. */
67
+ export type MessageOf<Y> = Exclude<Y, NamedEvent<string, unknown>>;
68
+ /** The data of the messages named `E`. A name computed at runtime matches any. */
69
+ export type NamedMessageOf<Y, E extends string> = Y extends NamedEvent<infer N, infer D> ? (E extends N ? D : never) : never;
70
+ export declare function events<P = Record<string, string>, Y = unknown>(produce: (input: EventsInput<P>) => AsyncIterable<Y>, options?: {
71
+ keepaliveMs?: number;
72
+ }): EventsRoute<P, Y>;
57
73
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A route that streams: server-sent events from an async generator.\n *\n * // app/api/orders/[id]/events/route.ts\n * export const GET = events(async function* ({ params, signal }) {\n * for await (const status of orderStatus((await params).id, { signal })) {\n * yield { status }\n * }\n * })\n *\n * An ordinary route.ts, so it lives beside its pages, runs the middleware.ts\n * above it and takes the same params. What this adds is the framing - one\n * `data:` line per yield, JSON - a keepalive comment every fifteen seconds\n * so a proxy does not drop an idle stream, the headers a stream needs, and\n * an end to the generator when the browser goes away, which is what `signal`\n * is for. `useEvents` on the client reads it.\n *\n * Not a query. A query answers once and is cacheable; a stream answers for\n * as long as the tab is open and is not. A server component cannot await a\n * stream, so it has no place in the payload either.\n */\n\nexport interface EventsInput<P = Record<string, string>> {\n params: Promise<P>;\n searchParams: Promise<URLSearchParams>;\n /** Aborted when the browser disconnects. Pass it on, or check `aborted`. */\n signal: AbortSignal;\n request: Request;\n}\n\n/** A message with a name or an id, for a client that listens by name or resumes. Made by `named()`. */\nexport interface NamedEvent {\n readonly [NAMED]: true;\n event?: string;\n id?: string;\n data: unknown;\n}\n\nconst NAMED = Symbol.for(\"@rsc-kit/core.named-event\");\n\n/**\n * A message with a name or an id: `yield named('paid', order)`, or\n * `named('paid', order, { id: order.id })` for one a client can resume from.\n * Built by a function rather than by shape, so a message that happens to\n * have a `data` key is still data, and the generator's own type stays the\n * type of what it yields.\n */\nexport function named<D>(\n event: string,\n data: D,\n options: { id?: string } = {},\n): NamedEvent {\n return { [NAMED]: true, event, id: options.id, data };\n}\n\n/** How often a comment goes out on an idle stream, so a proxy keeps it open. */\nexport const KEEPALIVE_MS = 15_000;\n\nfunction isNamed(value: unknown): value is NamedEvent {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { [NAMED]?: true })[NAMED] === true\n );\n}\n\n/** One message, framed. */\nexport function frame(value: unknown): string {\n const named = isNamed(value) ? value : null;\n const lines: string[] = [];\n\n if (named?.event) lines.push(`event: ${named.event}`);\n if (named?.id) lines.push(`id: ${named.id}`);\n lines.push(`data: ${JSON.stringify(named ? named.data : value)}`);\n\n return lines.join(\"\\n\") + \"\\n\\n\";\n}\n\nexport function events<P = Record<string, string>, T = unknown>(\n produce: (input: EventsInput<P>) => AsyncIterable<T | NamedEvent>,\n options: { keepaliveMs?: number } = {},\n): (\n request: Request,\n input: { params: Promise<P>; searchParams: Promise<URLSearchParams> },\n) => Response {\n const keepaliveMs = options.keepaliveMs ?? KEEPALIVE_MS;\n\n return (request, input) => {\n const controller = new AbortController();\n const encoder = new TextEncoder();\n\n request.signal.addEventListener(\"abort\", () => controller.abort(), {\n once: true,\n });\n\n const stream = new ReadableStream<Uint8Array>({\n async start(sink) {\n // The retry hint first, so a browser that loses the connection waits\n // a moment rather than hammering.\n sink.enqueue(encoder.encode(\"retry: 1000\\n\\n\"));\n\n const keepalive = setInterval(() => {\n try {\n sink.enqueue(encoder.encode(\": keepalive\\n\\n\"));\n } catch {\n // Closed between the check and the write; the loop below ends it.\n }\n }, keepaliveMs);\n\n try {\n for await (const value of produce({\n ...input,\n signal: controller.signal,\n request,\n })) {\n if (controller.signal.aborted) break;\n\n sink.enqueue(encoder.encode(frame(value)));\n }\n } catch (error) {\n if (!controller.signal.aborted) sink.error(error);\n } finally {\n clearInterval(keepalive);\n\n if (!controller.signal.aborted) {\n try {\n sink.close();\n } catch {\n // Already closed by the consumer.\n }\n }\n }\n },\n cancel() {\n controller.abort();\n },\n });\n\n return new Response(stream, {\n headers: {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n // Nginx buffers by default, which turns a stream into one late burst.\n \"X-Accel-Buffering\": \"no\",\n },\n });\n };\n}\n"]}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A route that streams: server-sent events from an async generator.\n *\n * // app/api/orders/[id]/events/route.ts\n * export const GET = events(async function* ({ params, signal }) {\n * for await (const status of orderStatus((await params).id, { signal })) {\n * yield { status }\n * }\n * })\n *\n * An ordinary route.ts, so it lives beside its pages, runs the middleware.ts\n * above it and takes the same params. What this adds is the framing - one\n * `data:` line per yield, JSON - a keepalive comment every fifteen seconds\n * so a proxy does not drop an idle stream, the headers a stream needs, and\n * an end to the generator when the browser goes away, which is what `signal`\n * is for. `useEvents` on the client reads it.\n *\n * Not a query. A query answers once and is cacheable; a stream answers for\n * as long as the tab is open and is not. A server component cannot await a\n * stream, so it has no place in the payload either.\n */\n\nexport interface EventsInput<P = Record<string, string>> {\n params: Promise<P>;\n searchParams: Promise<URLSearchParams>;\n /** Aborted when the browser disconnects. Pass it on, or check `aborted`. */\n signal: AbortSignal;\n request: Request;\n}\n\n/** A message with a name or an id, for a client that listens by name or resumes. Made by `named()`. */\nexport interface NamedEvent<E extends string = string, D = unknown> {\n readonly [NAMED]: true;\n event?: E;\n id?: string;\n data: D;\n}\n\nconst NAMED = Symbol.for(\"@rsc-kit/core.named-event\");\n\n/**\n * A message with a name or an id: `yield named('paid', order)`, or\n * `named('paid', order, { id: order.id })` for one a client can resume from.\n * Built by a function rather than by shape, so a message that happens to\n * have a `data` key is still data, and the generator's own type stays the\n * type of what it yields.\n */\nexport function named<const E extends string, D>(\n event: E,\n data: D,\n options: { id?: string } = {},\n): NamedEvent<E, D> {\n return { [NAMED]: true, event, id: options.id, data };\n}\n\n/** How often a comment goes out on an idle stream, so a proxy keeps it open. */\nexport const KEEPALIVE_MS = 15_000;\n\nfunction isNamed(value: unknown): value is NamedEvent {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as { [NAMED]?: true })[NAMED] === true\n );\n}\n\n/** One message, framed. */\nexport function frame(value: unknown): string {\n const named = isNamed(value) ? value : null;\n const lines: string[] = [];\n\n if (named?.event) lines.push(`event: ${named.event}`);\n if (named?.id) lines.push(`id: ${named.id}`);\n lines.push(`data: ${JSON.stringify(named ? named.data : value)}`);\n\n return lines.join(\"\\n\") + \"\\n\\n\";\n}\n\n/**\n * What `events()` returns: a GET handler that also says, as a type only, what\n * its generator yields. Nothing reads `~events` at runtime; the build records\n * it per url, which is how `useEvents('/api/orders/42/events')` types a\n * message without being told - and stops compiling when the route changes\n * what it sends.\n */\nexport type EventsRoute<P = Record<string, string>, Y = unknown> = ((\n request: Request,\n input: { params: Promise<P>; searchParams: Promise<URLSearchParams> },\n) => Response) & { readonly \"~events\"?: { yields: Y } };\n\n/** The unnamed messages among a stream's yields: what `useEvents` receives without `event`. */\nexport type MessageOf<Y> = Exclude<Y, NamedEvent<string, unknown>>;\n\n/** The data of the messages named `E`. A name computed at runtime matches any. */\nexport type NamedMessageOf<Y, E extends string> =\n Y extends NamedEvent<infer N, infer D> ? (E extends N ? D : never) : never;\n\nexport function events<P = Record<string, string>, Y = unknown>(\n produce: (input: EventsInput<P>) => AsyncIterable<Y>,\n options: { keepaliveMs?: number } = {},\n): EventsRoute<P, Y> {\n const keepaliveMs = options.keepaliveMs ?? KEEPALIVE_MS;\n\n return (\n request: Request,\n input: { params: Promise<P>; searchParams: Promise<URLSearchParams> },\n ) => {\n const controller = new AbortController();\n const encoder = new TextEncoder();\n\n request.signal.addEventListener(\"abort\", () => controller.abort(), {\n once: true,\n });\n\n const stream = new ReadableStream<Uint8Array>({\n async start(sink) {\n // The retry hint first, so a browser that loses the connection waits\n // a moment rather than hammering.\n sink.enqueue(encoder.encode(\"retry: 1000\\n\\n\"));\n\n const keepalive = setInterval(() => {\n try {\n sink.enqueue(encoder.encode(\": keepalive\\n\\n\"));\n } catch {\n // Closed between the check and the write; the loop below ends it.\n }\n }, keepaliveMs);\n\n try {\n for await (const value of produce({\n ...input,\n signal: controller.signal,\n request,\n })) {\n if (controller.signal.aborted) break;\n\n sink.enqueue(encoder.encode(frame(value)));\n }\n } catch (error) {\n if (!controller.signal.aborted) sink.error(error);\n } finally {\n clearInterval(keepalive);\n\n if (!controller.signal.aborted) {\n try {\n sink.close();\n } catch {\n // Already closed by the consumer.\n }\n }\n }\n },\n cancel() {\n controller.abort();\n },\n });\n\n return new Response(stream, {\n headers: {\n \"Content-Type\": \"text/event-stream; charset=utf-8\",\n \"Cache-Control\": \"no-store\",\n // Nginx buffers by default, which turns a stream into one late burst.\n \"X-Accel-Buffering\": \"no\",\n },\n });\n };\n}\n"]}
package/dist/host.js CHANGED
@@ -186,12 +186,15 @@ function refusalStatus(error) {
186
186
  return 401;
187
187
  if (name === "ServerAuthorizationError")
188
188
  return 403;
189
- // A status the host chose — a throttle's 429, a policy's 403 — carried on
190
- // the error by the transport. Bounded to refusals: a host answering 500
191
- // should not be able to make this look like a client's fault, and one
192
- // answering 200 should not turn a failed render into a success.
189
+ // A status the host chose — a throttle's 429, a policy's 403, a 503 while
190
+ // a dependency is down or the app is in maintenance — carried on the error
191
+ // by the transport. Only a deliberate refusal carries one (Go's Refuse,
192
+ // Laravel's abort); a crash never does, so a 5xx here was meant, and
193
+ // collapsing a 503 to a 500 tells a client to give up rather than come
194
+ // back. Bounded to errors: a host answering 200 or 3xx should not turn a
195
+ // failed render into a success or a redirect with no Location.
193
196
  const carried = error?.refusalStatus;
194
- if (typeof carried === "number" && carried >= 400 && carried <= 499)
197
+ if (typeof carried === "number" && carried >= 400 && carried <= 599)
195
198
  return carried;
196
199
  return null;
197
200
  }