@rsc-kit/core 0.17.0 → 0.18.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/barrelImports.d.ts +6 -0
- package/dist/barrelImports.js +93 -0
- package/dist/barrelImports.js.map +1 -0
- package/dist/events.d.ts +57 -0
- package/dist/events.js +113 -0
- package/dist/events.js.map +1 -0
- package/dist/host.d.ts +16 -6
- package/dist/host.js +219 -124
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +27 -0
- package/dist/hostCalls.js +127 -12
- package/dist/hostCalls.js.map +1 -1
- package/dist/hostRouting.d.ts +53 -0
- package/dist/hostRouting.js +100 -0
- package/dist/hostRouting.js.map +1 -0
- package/dist/js/DefaultRouteError.d.ts +3 -0
- package/dist/js/DefaultRouteError.js +61 -0
- package/dist/js/DefaultRouteError.js.map +1 -0
- package/dist/js/queryClient.d.ts +2 -1
- package/dist/js/queryClient.js +0 -12
- package/dist/js/queryClient.js.map +1 -1
- package/dist/js/router.d.ts +2 -2
- package/dist/js/router.js.map +1 -1
- package/dist/js/useEvents.d.ts +25 -0
- package/dist/js/useEvents.js +78 -0
- package/dist/js/useEvents.js.map +1 -0
- package/dist/js/usePolling.d.ts +29 -0
- package/dist/js/usePolling.js +141 -0
- package/dist/js/usePolling.js.map +1 -0
- package/dist/manifest.d.ts +18 -1
- package/dist/manifest.js.map +1 -1
- package/dist/metadataRoutes.d.ts +20 -0
- package/dist/metadataRoutes.js +35 -0
- package/dist/metadataRoutes.js.map +1 -1
- package/dist/prerender.js +1 -0
- package/dist/prerender.js.map +1 -1
- package/dist/redirect.d.ts +16 -2
- package/dist/redirect.js +10 -19
- package/dist/redirect.js.map +1 -1
- package/dist/revalidate.d.ts +6 -4
- package/dist/revalidate.js +6 -5
- package/dist/revalidate.js.map +1 -1
- package/dist/routes.d.ts +16 -3
- package/dist/routes.js +5 -5
- package/dist/routes.js.map +1 -1
- package/dist/routing.d.ts +2 -2
- package/dist/routing.js +47 -25
- package/dist/routing.js.map +1 -1
- package/dist/vite.d.ts +42 -2
- package/dist/vite.js +333 -5
- package/dist/vite.js.map +1 -1
- package/package.json +13 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite the named imports in `code` whose package resolves to a barrel.
|
|
3
|
+
* `resolveEntry` answers a bare specifier with the file Vite would load for
|
|
4
|
+
* it, or null. Returns null when nothing changed.
|
|
5
|
+
*/
|
|
6
|
+
export declare function unrollBarrelImports(code: string, resolveEntry: (specifier: string) => string | null): string | null;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* A named import from a barrel, rewritten to the module that owns the name.
|
|
5
|
+
*
|
|
6
|
+
* `import { ArrowRight } from 'lucide-react'` reaches a file that re-exports
|
|
7
|
+
* fifteen hundred icons, one module each. The browser gets the package
|
|
8
|
+
* pre-bundled; the server environments do not, on purpose - a package with
|
|
9
|
+
* a "use client" file stays as files so a client reference resolves to a
|
|
10
|
+
* real one - so Vite loaded and transformed every icon module on the first
|
|
11
|
+
* request. Measured: 3,700 transforms and twenty-five seconds before the
|
|
12
|
+
* first document, for thirteen icons. Next rewrites these imports for the
|
|
13
|
+
* same reason (optimizePackageImports); this does it for the packages it
|
|
14
|
+
* finds barrels in, by reading the barrel.
|
|
15
|
+
*
|
|
16
|
+
* The map from export name to file is read from the barrel's own
|
|
17
|
+
* `export { default as Name } from './x.mjs'` lines, once per barrel, so no
|
|
18
|
+
* package needs a table here. A name the barrel does not map that way - a
|
|
19
|
+
* component exported from the package's own source, say - is left on the
|
|
20
|
+
* original import and resolves as before.
|
|
21
|
+
*/
|
|
22
|
+
const REEXPORT = /export\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
|
|
23
|
+
const NAMED_IMPORT = /^([ \t]*)import\s*(type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\4\s*;?[ \t]*$/gm;
|
|
24
|
+
/** Which packages to unroll: a barrel is any entry whose exports are mostly re-exports of default exports. */
|
|
25
|
+
const barrels = new Map();
|
|
26
|
+
function readBarrel(entry) {
|
|
27
|
+
const cached = barrels.get(entry);
|
|
28
|
+
if (cached !== undefined)
|
|
29
|
+
return cached;
|
|
30
|
+
let map = null;
|
|
31
|
+
try {
|
|
32
|
+
const source = readFileSync(entry, "utf-8");
|
|
33
|
+
const found = new Map();
|
|
34
|
+
for (const m of source.matchAll(REEXPORT)) {
|
|
35
|
+
const file = resolve(dirname(entry), m[2]);
|
|
36
|
+
for (const spec of m[1].split(",")) {
|
|
37
|
+
const part = spec.trim().match(/^default\s+as\s+([A-Za-z_$][\w$]*)$/);
|
|
38
|
+
if (part)
|
|
39
|
+
found.set(part[1], file);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Under a hundred names is a module with a few re-exports, not a barrel
|
|
43
|
+
// worth unrolling; the cost this pays for is the long tail.
|
|
44
|
+
map = found.size >= 100 ? found : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
map = null;
|
|
48
|
+
}
|
|
49
|
+
barrels.set(entry, map);
|
|
50
|
+
return map;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Rewrite the named imports in `code` whose package resolves to a barrel.
|
|
54
|
+
* `resolveEntry` answers a bare specifier with the file Vite would load for
|
|
55
|
+
* it, or null. Returns null when nothing changed.
|
|
56
|
+
*/
|
|
57
|
+
export function unrollBarrelImports(code, resolveEntry) {
|
|
58
|
+
if (!code.includes("import"))
|
|
59
|
+
return null;
|
|
60
|
+
let changed = false;
|
|
61
|
+
const out = code.replace(NAMED_IMPORT, (whole, indent, typeOnly, names, _q, specifier) => {
|
|
62
|
+
if (typeOnly || specifier.startsWith(".") || specifier.startsWith("/"))
|
|
63
|
+
return whole;
|
|
64
|
+
const entry = resolveEntry(specifier);
|
|
65
|
+
const map = entry && existsSync(entry) ? readBarrel(entry) : null;
|
|
66
|
+
if (!map)
|
|
67
|
+
return whole;
|
|
68
|
+
const kept = [];
|
|
69
|
+
const direct = [];
|
|
70
|
+
for (const spec of names.split(",")) {
|
|
71
|
+
const s = spec.trim();
|
|
72
|
+
if (!s)
|
|
73
|
+
continue;
|
|
74
|
+
const m = s.match(/^(?:type\s+)?([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
|
|
75
|
+
const exported = m?.[1];
|
|
76
|
+
const local = m?.[2] ?? exported;
|
|
77
|
+
const file = exported && !s.startsWith("type ") ? map.get(exported) : undefined;
|
|
78
|
+
if (file && local)
|
|
79
|
+
direct.push(`${indent}import ${local} from ${JSON.stringify(file)};`);
|
|
80
|
+
else
|
|
81
|
+
kept.push(s);
|
|
82
|
+
}
|
|
83
|
+
if (direct.length === 0)
|
|
84
|
+
return whole;
|
|
85
|
+
changed = true;
|
|
86
|
+
const rest = kept.length
|
|
87
|
+
? `${indent}import { ${kept.join(", ")} } from ${JSON.stringify(specifier)};\n`
|
|
88
|
+
: "";
|
|
89
|
+
return rest + direct.join("\n");
|
|
90
|
+
});
|
|
91
|
+
return changed ? out : null;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=barrelImports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"barrelImports.js","sourceRoot":"","sources":["../src/barrelImports.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\n\n/**\n * A named import from a barrel, rewritten to the module that owns the name.\n *\n * `import { ArrowRight } from 'lucide-react'` reaches a file that re-exports\n * fifteen hundred icons, one module each. The browser gets the package\n * pre-bundled; the server environments do not, on purpose - a package with\n * a \"use client\" file stays as files so a client reference resolves to a\n * real one - so Vite loaded and transformed every icon module on the first\n * request. Measured: 3,700 transforms and twenty-five seconds before the\n * first document, for thirteen icons. Next rewrites these imports for the\n * same reason (optimizePackageImports); this does it for the packages it\n * finds barrels in, by reading the barrel.\n *\n * The map from export name to file is read from the barrel's own\n * `export { default as Name } from './x.mjs'` lines, once per barrel, so no\n * package needs a table here. A name the barrel does not map that way - a\n * component exported from the package's own source, say - is left on the\n * original import and resolves as before.\n */\n\nconst REEXPORT = /export\\s*\\{([^}]*)\\}\\s*from\\s*['\"]([^'\"]+)['\"]/g;\nconst NAMED_IMPORT =\n /^([ \\t]*)import\\s*(type\\s+)?\\{([^}]*)\\}\\s*from\\s*(['\"])([^'\"]+)\\4\\s*;?[ \\t]*$/gm;\n\n/** Which packages to unroll: a barrel is any entry whose exports are mostly re-exports of default exports. */\nconst barrels = new Map<string, Map<string, string> | null>();\n\nfunction readBarrel(entry: string): Map<string, string> | null {\n const cached = barrels.get(entry);\n\n if (cached !== undefined) return cached;\n\n let map: Map<string, string> | null = null;\n\n try {\n const source = readFileSync(entry, \"utf-8\");\n const found = new Map<string, string>();\n\n for (const m of source.matchAll(REEXPORT)) {\n const file = resolve(dirname(entry), m[2]);\n\n for (const spec of m[1].split(\",\")) {\n const part = spec.trim().match(/^default\\s+as\\s+([A-Za-z_$][\\w$]*)$/);\n\n if (part) found.set(part[1], file);\n }\n }\n\n // Under a hundred names is a module with a few re-exports, not a barrel\n // worth unrolling; the cost this pays for is the long tail.\n map = found.size >= 100 ? found : null;\n } catch {\n map = null;\n }\n\n barrels.set(entry, map);\n\n return map;\n}\n\n/**\n * Rewrite the named imports in `code` whose package resolves to a barrel.\n * `resolveEntry` answers a bare specifier with the file Vite would load for\n * it, or null. Returns null when nothing changed.\n */\nexport function unrollBarrelImports(\n code: string,\n resolveEntry: (specifier: string) => string | null,\n): string | null {\n if (!code.includes(\"import\")) return null;\n\n let changed = false;\n\n const out = code.replace(\n NAMED_IMPORT,\n (whole, indent: string, typeOnly, names: string, _q, specifier: string) => {\n if (typeOnly || specifier.startsWith(\".\") || specifier.startsWith(\"/\"))\n return whole;\n\n const entry = resolveEntry(specifier);\n const map = entry && existsSync(entry) ? readBarrel(entry) : null;\n\n if (!map) return whole;\n\n const kept: string[] = [];\n const direct: string[] = [];\n\n for (const spec of names.split(\",\")) {\n const s = spec.trim();\n\n if (!s) continue;\n\n const m = s.match(\n /^(?:type\\s+)?([A-Za-z_$][\\w$]*)(?:\\s+as\\s+([A-Za-z_$][\\w$]*))?$/,\n );\n const exported = m?.[1];\n const local = m?.[2] ?? exported;\n const file =\n exported && !s.startsWith(\"type \") ? map.get(exported) : undefined;\n\n if (file && local)\n direct.push(`${indent}import ${local} from ${JSON.stringify(file)};`);\n else kept.push(s);\n }\n\n if (direct.length === 0) return whole;\n\n changed = true;\n\n const rest = kept.length\n ? `${indent}import { ${kept.join(\", \")} } from ${JSON.stringify(specifier)};\\n`\n : \"\";\n\n return rest + direct.join(\"\\n\");\n },\n );\n\n return changed ? out : null;\n}\n"]}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A route that streams: server-sent events from an async generator.
|
|
3
|
+
*
|
|
4
|
+
* // app/api/orders/[id]/events/route.ts
|
|
5
|
+
* export const GET = events(async function* ({ params, signal }) {
|
|
6
|
+
* for await (const status of orderStatus((await params).id, { signal })) {
|
|
7
|
+
* yield { status }
|
|
8
|
+
* }
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* An ordinary route.ts, so it lives beside its pages, runs the middleware.ts
|
|
12
|
+
* above it and takes the same params. What this adds is the framing - one
|
|
13
|
+
* `data:` line per yield, JSON - a keepalive comment every fifteen seconds
|
|
14
|
+
* so a proxy does not drop an idle stream, the headers a stream needs, and
|
|
15
|
+
* an end to the generator when the browser goes away, which is what `signal`
|
|
16
|
+
* is for. `useEvents` on the client reads it.
|
|
17
|
+
*
|
|
18
|
+
* Not a query. A query answers once and is cacheable; a stream answers for
|
|
19
|
+
* as long as the tab is open and is not. A server component cannot await a
|
|
20
|
+
* stream, so it has no place in the payload either.
|
|
21
|
+
*/
|
|
22
|
+
export interface EventsInput<P = Record<string, string>> {
|
|
23
|
+
params: Promise<P>;
|
|
24
|
+
searchParams: Promise<URLSearchParams>;
|
|
25
|
+
/** Aborted when the browser disconnects. Pass it on, or check `aborted`. */
|
|
26
|
+
signal: AbortSignal;
|
|
27
|
+
request: Request;
|
|
28
|
+
}
|
|
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 {
|
|
31
|
+
readonly [NAMED]: true;
|
|
32
|
+
event?: string;
|
|
33
|
+
id?: string;
|
|
34
|
+
data: unknown;
|
|
35
|
+
}
|
|
36
|
+
declare const NAMED: unique symbol;
|
|
37
|
+
/**
|
|
38
|
+
* A message with a name or an id: `yield named('paid', order)`, or
|
|
39
|
+
* `named('paid', order, { id: order.id })` for one a client can resume from.
|
|
40
|
+
* Built by a function rather than by shape, so a message that happens to
|
|
41
|
+
* have a `data` key is still data, and the generator's own type stays the
|
|
42
|
+
* type of what it yields.
|
|
43
|
+
*/
|
|
44
|
+
export declare function named<D>(event: string, data: D, options?: {
|
|
45
|
+
id?: string;
|
|
46
|
+
}): NamedEvent;
|
|
47
|
+
/** How often a comment goes out on an idle stream, so a proxy keeps it open. */
|
|
48
|
+
export declare const KEEPALIVE_MS = 15000;
|
|
49
|
+
/** One message, framed. */
|
|
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: {
|
|
54
|
+
params: Promise<P>;
|
|
55
|
+
searchParams: Promise<URLSearchParams>;
|
|
56
|
+
}) => Response;
|
|
57
|
+
export {};
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A route that streams: server-sent events from an async generator.
|
|
3
|
+
*
|
|
4
|
+
* // app/api/orders/[id]/events/route.ts
|
|
5
|
+
* export const GET = events(async function* ({ params, signal }) {
|
|
6
|
+
* for await (const status of orderStatus((await params).id, { signal })) {
|
|
7
|
+
* yield { status }
|
|
8
|
+
* }
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* An ordinary route.ts, so it lives beside its pages, runs the middleware.ts
|
|
12
|
+
* above it and takes the same params. What this adds is the framing - one
|
|
13
|
+
* `data:` line per yield, JSON - a keepalive comment every fifteen seconds
|
|
14
|
+
* so a proxy does not drop an idle stream, the headers a stream needs, and
|
|
15
|
+
* an end to the generator when the browser goes away, which is what `signal`
|
|
16
|
+
* is for. `useEvents` on the client reads it.
|
|
17
|
+
*
|
|
18
|
+
* Not a query. A query answers once and is cacheable; a stream answers for
|
|
19
|
+
* as long as the tab is open and is not. A server component cannot await a
|
|
20
|
+
* stream, so it has no place in the payload either.
|
|
21
|
+
*/
|
|
22
|
+
const NAMED = Symbol.for("@rsc-kit/core.named-event");
|
|
23
|
+
/**
|
|
24
|
+
* A message with a name or an id: `yield named('paid', order)`, or
|
|
25
|
+
* `named('paid', order, { id: order.id })` for one a client can resume from.
|
|
26
|
+
* Built by a function rather than by shape, so a message that happens to
|
|
27
|
+
* have a `data` key is still data, and the generator's own type stays the
|
|
28
|
+
* type of what it yields.
|
|
29
|
+
*/
|
|
30
|
+
export function named(event, data, options = {}) {
|
|
31
|
+
return { [NAMED]: true, event, id: options.id, data };
|
|
32
|
+
}
|
|
33
|
+
/** How often a comment goes out on an idle stream, so a proxy keeps it open. */
|
|
34
|
+
export const KEEPALIVE_MS = 15_000;
|
|
35
|
+
function isNamed(value) {
|
|
36
|
+
return (typeof value === "object" &&
|
|
37
|
+
value !== null &&
|
|
38
|
+
value[NAMED] === true);
|
|
39
|
+
}
|
|
40
|
+
/** One message, framed. */
|
|
41
|
+
export function frame(value) {
|
|
42
|
+
const named = isNamed(value) ? value : null;
|
|
43
|
+
const lines = [];
|
|
44
|
+
if (named?.event)
|
|
45
|
+
lines.push(`event: ${named.event}`);
|
|
46
|
+
if (named?.id)
|
|
47
|
+
lines.push(`id: ${named.id}`);
|
|
48
|
+
lines.push(`data: ${JSON.stringify(named ? named.data : value)}`);
|
|
49
|
+
return lines.join("\n") + "\n\n";
|
|
50
|
+
}
|
|
51
|
+
export function events(produce, options = {}) {
|
|
52
|
+
const keepaliveMs = options.keepaliveMs ?? KEEPALIVE_MS;
|
|
53
|
+
return (request, input) => {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const encoder = new TextEncoder();
|
|
56
|
+
request.signal.addEventListener("abort", () => controller.abort(), {
|
|
57
|
+
once: true,
|
|
58
|
+
});
|
|
59
|
+
const stream = new ReadableStream({
|
|
60
|
+
async start(sink) {
|
|
61
|
+
// The retry hint first, so a browser that loses the connection waits
|
|
62
|
+
// a moment rather than hammering.
|
|
63
|
+
sink.enqueue(encoder.encode("retry: 1000\n\n"));
|
|
64
|
+
const keepalive = setInterval(() => {
|
|
65
|
+
try {
|
|
66
|
+
sink.enqueue(encoder.encode(": keepalive\n\n"));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Closed between the check and the write; the loop below ends it.
|
|
70
|
+
}
|
|
71
|
+
}, keepaliveMs);
|
|
72
|
+
try {
|
|
73
|
+
for await (const value of produce({
|
|
74
|
+
...input,
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
request,
|
|
77
|
+
})) {
|
|
78
|
+
if (controller.signal.aborted)
|
|
79
|
+
break;
|
|
80
|
+
sink.enqueue(encoder.encode(frame(value)));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (!controller.signal.aborted)
|
|
85
|
+
sink.error(error);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
clearInterval(keepalive);
|
|
89
|
+
if (!controller.signal.aborted) {
|
|
90
|
+
try {
|
|
91
|
+
sink.close();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Already closed by the consumer.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
cancel() {
|
|
100
|
+
controller.abort();
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return new Response(stream, {
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
106
|
+
"Cache-Control": "no-store",
|
|
107
|
+
// Nginx buffers by default, which turns a stream into one late burst.
|
|
108
|
+
"X-Accel-Buffering": "no",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +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"]}
|
package/dist/host.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @internal For a host adapter that embeds the engine. An app imports this
|
|
3
|
+
* from `@rsc-kit/core/revalidate`.
|
|
4
|
+
*/
|
|
5
|
+
export { revalidate } from "./revalidate.js";
|
|
6
|
+
/**
|
|
7
|
+
* @internal For a host adapter that embeds the engine. An app imports this
|
|
8
|
+
* from `@rsc-kit/core/redirect` - the one the guides teach, and the one an
|
|
9
|
+
* editor should offer first.
|
|
10
|
+
*/
|
|
11
|
+
export { redirect } from "./redirect.js";
|
|
12
|
+
/** @internal For a host adapter. An app never matches its own routes. */
|
|
13
|
+
export { matchIntercept, matchRoute, sharedDepth } from "./routing.js";
|
|
14
|
+
export type { MatchedRoute } from "./routing.js";
|
|
15
|
+
import type { MatchedRoute } from "./routing.js";
|
|
16
|
+
import type { RouteManifest } from "./manifest.js";
|
|
7
17
|
/** The built server bundle. Only the parts a host calls. */
|
|
8
18
|
export interface RscEngine {
|
|
9
19
|
/** The route table this bundle was built from. */
|