@rsc-kit/core 0.30.43 → 0.30.45

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/host.js CHANGED
@@ -19,7 +19,7 @@ import { apiKey } from "./apiPrerender.js";
19
19
  import { hostPath, hostSegment, routableHost } from "./hostRouting.js";
20
20
  import { withRevalidation } from "./revalidate.js";
21
21
  import { withoutPayloadKey } from "./js/payloadKey.js";
22
- import { cachingFor, finishCacheable, noteReadsSoFar } from "./pageCache.js";
22
+ import { cachingFor, createPageCache, finishCacheable, noteReadsSoFar, pageMemoryStore } from "./pageCache.js";
23
23
  /**
24
24
  * @internal For a host adapter that embeds the engine. An app imports this
25
25
  * from `@rsc-kit/core/revalidate`.
@@ -69,7 +69,7 @@ import { isCrawler } from "./crawler.js";
69
69
  import { inlineFlight, inlineFlightStart } from "./inlineFlight.js";
70
70
  import { currentNotFound, withRedirect } from "./redirect.js";
71
71
  import { compressed } from "./compress.js";
72
- import { setCacheBuild, withCache } from "./cache.js";
72
+ import { installedCacheStore, setCacheBuild, withCache } from "./cache.js";
73
73
  import { notFoundRowSent, renderAsNobody, takeAfterWork, withRequest, withResponseDraft, withUrl } from "./request.js";
74
74
  import { criticalAssetsOf, linkHeader, mergeAssets } from "./earlyHints.js";
75
75
  /**
@@ -86,6 +86,19 @@ export { matchIntercept, matchRoute, sharedDepth } from "./routing.js";
86
86
  import { FLIGHT_TYPE, HEADER, HTML_TYPE, PER_CLIENT, PER_REQUEST, REVALIDATE, EDGE, VARY_ON_RSC, } from "./headers.js";
87
87
  import { ServerAuthenticationError, ServerAuthorizationError, } from "./js/errors.js";
88
88
  import { assertSigningSecret, serveChanges } from "./changed.js";
89
+ /**
90
+ * Whether instances live for a request or a few: a Worker's isolates, a
91
+ * Vercel function. Memory there is a store few requests ever find.
92
+ */
93
+ function shortLivedInstances() {
94
+ const env = globalThis.process?.env;
95
+ return globalThis.navigator?.userAgent === "Cloudflare-Workers" || Boolean(env?.VERCEL);
96
+ }
97
+ /** Whether the app runs on Homeport, whose edge keeps pages itself. */
98
+ function onHomeport() {
99
+ const env = globalThis.process?.env;
100
+ return Boolean(env?.HOMEPORT_PURGE_URL);
101
+ }
89
102
  /**
90
103
  * The answer to a redirect that was decided before anything was written.
91
104
  *
@@ -487,6 +500,10 @@ export function createRscHandler(options) {
487
500
  const maxActionBody = options.maxActionBody ?? DEFAULT_MAX_ACTION_BODY;
488
501
  const trustedOrigins = trustedOriginsOf(options.trustedOrigins);
489
502
  const compress = options.compress ?? true;
503
+ // The page cache, made on the first request: the app installs its store in
504
+ // register(), which runs after the handler is made.
505
+ let pages;
506
+ const pageCacheOption = onHomeport() ? false : (options.pageCache ?? true);
490
507
  // Annotated rather than inferred: the narrowing below is lost inside the
491
508
  // closures that use it, and every one of them runs after the throw.
492
509
  const manifest = options.manifest ?? engine.manifest?.();
@@ -692,6 +709,79 @@ export function createRscHandler(options) {
692
709
  const caching = cachingFor(arrived, page?.route.revalidate);
693
710
  if (caching && (page?.route.hostMiddleware?.length ?? 0) > 0)
694
711
  caching.refused ??= "host-middleware";
712
+ if (pages === undefined) {
713
+ const store = pageCacheOption === false ? null : installedCacheStore();
714
+ if (pageCacheOption !== false && !store && shortLivedInstances()) {
715
+ console.warn("[rsc-kit] revalidate pages are kept in this instance's memory, which this platform starts and stops per request: few requests will find one. Install a shared store with installCacheStore() - Workers KV, Redis - or set pageCache: false.");
716
+ }
717
+ pages =
718
+ pageCacheOption === false
719
+ ? null
720
+ : createPageCache(store ?? pageMemoryStore(typeof pageCacheOption === "object" ? pageCacheOption.maxBytes : undefined));
721
+ }
722
+ // The url as it arrived names the answer whole: the host, the query, and
723
+ // the ?_rsc= key that tells a document from each payload.
724
+ const pageKey = pages && caching && !caching.refused ? `${version ?? ""}|${arrived.url}` : null;
725
+ if (pages && pageKey) {
726
+ const hit = await pages.lookup(pageKey, arrived);
727
+ if (hit) {
728
+ if (hit.stale)
729
+ refreshKept(pages, pageKey, arrived, page.route);
730
+ return await finishKept(request, hit.response);
731
+ }
732
+ const run = await pages.once(pageKey, () => answer(request, caching, pageKey));
733
+ if (run.led)
734
+ return run.value ?? null;
735
+ // Another request rendered it while this one waited.
736
+ const after = await pages.lookup(pageKey, arrived);
737
+ if (after)
738
+ return await finishKept(request, after.response);
739
+ }
740
+ return await answer(request, caching, pageKey);
741
+ };
742
+ /**
743
+ * Render a kept page again behind a visitor who was just sent the stale
744
+ * copy - once per page at a time, in a request of its own: the visitor's
745
+ * may be cancelled the moment their answer is sent, and a HEAD or a
746
+ * conditional request is not the render to keep. A render that fails, or
747
+ * turns out private, keeps nothing, so the stale copy stays until its
748
+ * window ends.
749
+ */
750
+ function refreshKept(pages, pageKey, arrived, route) {
751
+ if (pages.busy(pageKey))
752
+ return;
753
+ const headers = new Headers(arrived.headers);
754
+ headers.delete("If-None-Match");
755
+ headers.delete("If-Modified-Since");
756
+ const fresh = new Request(arrived.url, { method: "GET", headers });
757
+ const caching = cachingFor(fresh, route.revalidate);
758
+ if (!caching)
759
+ return;
760
+ const work = pages
761
+ .once(pageKey, () => answer(withoutPayloadKey(fresh), caching, pageKey))
762
+ .then(async (run) => {
763
+ await run.value?.body?.cancel();
764
+ })
765
+ .catch((error) => {
766
+ console.warn(`[rsc-kit] refreshing the kept copy of ${caching.path} failed; the stale copy is served until it is rendered:`, error);
767
+ });
768
+ // A Worker keeps the isolate alive only for what is handed to waitUntil.
769
+ const context = arrived.context;
770
+ if (typeof context?.waitUntil === "function")
771
+ context.waitUntil(work);
772
+ }
773
+ /** A page the engine kept, sent the way a rendered one would be. */
774
+ async function finishKept(request, response) {
775
+ response.headers.set("X-RSC-Kit", "cached");
776
+ if (identify)
777
+ response.headers.set("X-Powered-By", "rsc-kit");
778
+ if (compress) {
779
+ const answer = await compressed(request, response, undefined);
780
+ return answer;
781
+ }
782
+ return response;
783
+ }
784
+ async function answer(request, caching, pageKey) {
695
785
  return await withRequest(request, () => withCache(() =>
696
786
  // Open for the whole request and sealed the moment an answer exists,
697
787
  // so middleware — which runs before any rendering — can put headers on
@@ -752,6 +842,9 @@ export function createRscHandler(options) {
752
842
  if (link)
753
843
  response.headers.set("Link", link);
754
844
  }
845
+ // Kept by the engine, when finishCacheable sent it public.
846
+ if (pages && pageKey)
847
+ await pages.keep(pageKey, response);
755
848
  // What built it. The name only, never the version - a version in
756
849
  // every response is what a vulnerability scanner filters on - and
757
850
  // off for a team whose policy strips every framework identifier.
@@ -784,7 +877,8 @@ export function createRscHandler(options) {
784
877
  }
785
878
  return response;
786
879
  })));
787
- };
880
+ }
881
+ ;
788
882
  async function route(request) {
789
883
  let url = new URL(request.url);
790
884
  // A host other than the site's own is matched with its segment in front