rwsdk 1.0.0-beta.41 → 1.0.0-beta.43

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.
Files changed (30) hide show
  1. package/dist/lib/e2e/dev.mjs +14 -10
  2. package/dist/lib/e2e/index.d.mts +1 -0
  3. package/dist/lib/e2e/index.mjs +1 -0
  4. package/dist/lib/e2e/release.mjs +9 -4
  5. package/dist/lib/e2e/testHarness.d.mts +9 -4
  6. package/dist/lib/e2e/testHarness.mjs +20 -2
  7. package/dist/runtime/client/client.d.ts +32 -4
  8. package/dist/runtime/client/client.js +98 -14
  9. package/dist/runtime/client/navigation.d.ts +6 -2
  10. package/dist/runtime/client/navigation.js +29 -17
  11. package/dist/runtime/client/navigationCache.d.ts +68 -0
  12. package/dist/runtime/client/navigationCache.js +294 -0
  13. package/dist/runtime/client/navigationCache.test.d.ts +1 -0
  14. package/dist/runtime/client/navigationCache.test.js +456 -0
  15. package/dist/runtime/client/types.d.ts +25 -3
  16. package/dist/runtime/client/types.js +7 -1
  17. package/dist/runtime/lib/realtime/client.js +17 -1
  18. package/dist/runtime/render/normalizeActionResult.js +8 -1
  19. package/dist/use-synced-state/SyncedStateServer.d.mts +19 -4
  20. package/dist/use-synced-state/SyncedStateServer.mjs +76 -8
  21. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +18 -11
  22. package/dist/use-synced-state/__tests__/worker.test.mjs +13 -12
  23. package/dist/use-synced-state/client-core.d.ts +3 -0
  24. package/dist/use-synced-state/client-core.js +77 -13
  25. package/dist/use-synced-state/useSyncedState.d.ts +3 -2
  26. package/dist/use-synced-state/useSyncedState.js +9 -3
  27. package/dist/use-synced-state/worker.d.mts +2 -1
  28. package/dist/use-synced-state/worker.mjs +82 -16
  29. package/dist/vite/transformClientComponents.test.mjs +32 -0
  30. package/package.json +7 -3
@@ -2,6 +2,7 @@ import debug from "debug";
2
2
  import { setTimeout as sleep } from "node:timers/promises";
3
3
  import { $, $sh } from "../../lib/$.mjs";
4
4
  import { poll } from "./poll.mjs";
5
+ import { IS_DEBUG_MODE } from "./constants.mjs";
5
6
  const DEV_SERVER_CHECK_TIMEOUT = process.env.RWSDK_DEV_SERVER_CHECK_TIMEOUT
6
7
  ? parseInt(process.env.RWSDK_DEV_SERVER_CHECK_TIMEOUT, 10)
7
8
  : 5 * 60 * 1000;
@@ -99,8 +100,10 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
99
100
  // Listen for all output to get the URL
100
101
  const handleOutput = (data, source) => {
101
102
  const output = data.toString();
102
- // Raw output for debugging
103
- process.stdout.write(`[dev:${source}] ` + output);
103
+ // Raw output for debugging - only in debug mode
104
+ if (IS_DEBUG_MODE) {
105
+ process.stdout.write(`[dev:${source}] ` + output);
106
+ }
104
107
  allOutput += output; // Accumulate all output
105
108
  if (!url) {
106
109
  // Multiple patterns to catch different package manager outputs
@@ -136,12 +139,16 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
136
139
  };
137
140
  // Listen to all possible output streams
138
141
  log("Setting up stream listeners. Available streams: all=%s, stdout=%s, stderr=%s", !!devProcess.all, !!devProcess.stdout, !!devProcess.stderr);
139
- devProcess.all?.on("data", (data) => handleOutput(data, "all"));
140
- devProcess.stdout?.on("data", (data) => handleOutput(data, "stdout"));
141
- devProcess.stderr?.on("data", (data) => handleOutput(data, "stderr"));
142
- // Also try listening to the raw process output
142
+ if (devProcess.all) {
143
+ devProcess.all.on("data", (data) => handleOutput(data, "all"));
144
+ }
145
+ else {
146
+ devProcess.stdout?.on("data", (data) => handleOutput(data, "stdout"));
147
+ devProcess.stderr?.on("data", (data) => handleOutput(data, "stderr"));
148
+ }
149
+ // Also try listening to the raw process events
143
150
  if (devProcess.child) {
144
- log("Setting up child process stream listeners");
151
+ log("Setting up child process events listeners");
145
152
  devProcess.child.on("spawn", () => {
146
153
  log("Child process spawned successfully.");
147
154
  });
@@ -151,8 +158,6 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
151
158
  devProcess.child.on("exit", (code, signal) => {
152
159
  log("Child process exited with code %s and signal %s", code, signal);
153
160
  });
154
- devProcess.child.stdout?.on("data", (data) => handleOutput(data, "child.stdout"));
155
- devProcess.child.stderr?.on("data", (data) => handleOutput(data, "child.stderr"));
156
161
  }
157
162
  // Wait for URL with timeout
158
163
  const waitForUrl = async () => {
@@ -164,7 +169,6 @@ export async function runDevServer(packageManager = "pnpm", cwd) {
164
169
  }
165
170
  // Fallback: check accumulated output if stream listeners aren't working
166
171
  if (!url && allOutput) {
167
- log("Checking accumulated output for URL patterns: %s", allOutput.replace(/\n/g, "\\n"));
168
172
  const patterns = [
169
173
  /Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
170
174
  /[➜→]\s*Local:\s*(?:\u001b\[\d+m)?(https?:\/\/localhost:\d+)/i,
@@ -6,4 +6,5 @@ export * from "./poll.mjs";
6
6
  export * from "./release.mjs";
7
7
  export * from "./tarball.mjs";
8
8
  export * from "./testHarness.mjs";
9
+ export { SKIP_DEPLOYMENT_TESTS, SKIP_DEV_SERVER_TESTS, } from "./testHarness.mjs";
9
10
  export * from "./types.mjs";
@@ -6,4 +6,5 @@ export * from "./poll.mjs";
6
6
  export * from "./release.mjs";
7
7
  export * from "./tarball.mjs";
8
8
  export * from "./testHarness.mjs";
9
+ export { SKIP_DEPLOYMENT_TESTS, SKIP_DEV_SERVER_TESTS, } from "./testHarness.mjs";
9
10
  export * from "./types.mjs";
@@ -8,6 +8,7 @@ import { setTimeout } from "node:timers/promises";
8
8
  import { basename, dirname, join, resolve } from "path";
9
9
  import { $ } from "../../lib/$.mjs";
10
10
  import { extractLastJson, parseJson } from "../../lib/jsonUtils.mjs";
11
+ import { IS_DEBUG_MODE } from "./constants.mjs";
11
12
  const log = debug("rwsdk:e2e:release");
12
13
  /**
13
14
  * Find wrangler cache by searching up the directory tree for node_modules/.cache/wrangler
@@ -68,8 +69,10 @@ export async function $expect(command, expectations, options = {
68
69
  const chunk = data.toString();
69
70
  stdout += chunk;
70
71
  buffer += chunk;
71
- // Print to console
72
- process.stdout.write(chunk);
72
+ // Print to console in debug mode
73
+ if (IS_DEBUG_MODE) {
74
+ process.stdout.write(chunk);
75
+ }
73
76
  // Only process expectations that haven't been fully matched yet
74
77
  // and in the order they were provided
75
78
  while (currentExpectationIndex < expectations.length) {
@@ -133,8 +136,10 @@ export async function $expect(command, expectations, options = {
133
136
  childProcess.stderr.on("data", (data) => {
134
137
  const chunk = data.toString();
135
138
  stderr += chunk;
136
- // Also write stderr to console
137
- process.stderr.write(chunk);
139
+ // Also write stderr to console in debug mode
140
+ if (IS_DEBUG_MODE) {
141
+ process.stderr.write(chunk);
142
+ }
138
143
  });
139
144
  }
140
145
  // Handle process completion
@@ -15,6 +15,8 @@ interface DeploymentInstance {
15
15
  projectDir: string;
16
16
  cleanup: () => Promise<void>;
17
17
  }
18
+ export declare const SKIP_DEV_SERVER_TESTS: boolean;
19
+ export declare const SKIP_DEPLOYMENT_TESTS: boolean;
18
20
  export interface SetupPlaygroundEnvironmentOptions {
19
21
  /**
20
22
  * The directory of the playground project to set up.
@@ -86,6 +88,12 @@ type SDKRunner = (name: string, testLogic: (context: {
86
88
  page: Page;
87
89
  projectDir: string;
88
90
  }) => Promise<void>) => void;
91
+ type SDKRunnerWithHelpers = SDKRunner & {
92
+ only: SDKRunner;
93
+ skip: typeof test.skip;
94
+ dev: SDKRunner;
95
+ deploy: SDKRunner;
96
+ };
89
97
  declare function createTestRunner(testFn: (typeof test | typeof test.only)["concurrent"], envType: "dev" | "deploy"): (name: string, testLogic: (context: {
90
98
  devServer?: DevServerInstance;
91
99
  deployment?: DeploymentInstance;
@@ -94,10 +102,7 @@ declare function createTestRunner(testFn: (typeof test | typeof test.only)["conc
94
102
  url: string;
95
103
  projectDir: string;
96
104
  }) => Promise<void>) => void;
97
- export declare const testSDK: SDKRunner & {
98
- only: SDKRunner;
99
- skip: typeof test.skip;
100
- };
105
+ export declare const testSDK: SDKRunnerWithHelpers;
101
106
  /**
102
107
  * High-level test wrapper for dev server tests.
103
108
  * Automatically skips if RWSDK_SKIP_DEV=1
@@ -12,8 +12,8 @@ import { setupTarballEnvironment } from "./tarball.mjs";
12
12
  import { ensureTmpDir } from "./utils.mjs";
13
13
  export { DEPLOYMENT_CHECK_TIMEOUT, DEPLOYMENT_MIN_TRIES, DEPLOYMENT_TIMEOUT, DEV_SERVER_MIN_TRIES, DEV_SERVER_TIMEOUT, HYDRATION_TIMEOUT, INSTALL_DEPENDENCIES_RETRIES, PUPPETEER_TIMEOUT, SETUP_PLAYGROUND_ENV_TIMEOUT, SETUP_WAIT_TIMEOUT, TEST_MAX_RETRIES, TEST_MAX_RETRIES_PER_CODE, };
14
14
  // Environment variable flags for skipping tests
15
- const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
16
- const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
15
+ export const SKIP_DEV_SERVER_TESTS = process.env.RWSDK_SKIP_DEV === "1";
16
+ export const SKIP_DEPLOYMENT_TESTS = process.env.RWSDK_SKIP_DEPLOY === "1";
17
17
  // Global test environment state
18
18
  let globalDevPlaygroundEnv = null;
19
19
  let globalDeployPlaygroundEnv = null;
@@ -461,9 +461,27 @@ function createSDKTestRunner() {
461
461
  };
462
462
  };
463
463
  const main = internalRunner(test);
464
+ // Helper method for dev-specific tests that respects SKIP_DEV_SERVER_TESTS
465
+ const dev = (name, testLogic) => {
466
+ if (SKIP_DEV_SERVER_TESTS) {
467
+ test.skip(`${name} (dev)`, () => { });
468
+ return;
469
+ }
470
+ return main(`${name} (dev)`, testLogic);
471
+ };
472
+ // Helper method for deploy-specific tests that respects SKIP_DEPLOYMENT_TESTS
473
+ const deploy = (name, testLogic) => {
474
+ if (SKIP_DEPLOYMENT_TESTS) {
475
+ test.skip(`${name} (deployment)`, () => { });
476
+ return;
477
+ }
478
+ return main(`${name} (deployment)`, testLogic);
479
+ };
464
480
  return Object.assign(main, {
465
481
  only: internalRunner(test.only),
466
482
  skip: test.skip,
483
+ dev,
484
+ deploy,
467
485
  });
468
486
  }
469
487
  export const testSDK = createSDKTestRunner();
@@ -3,7 +3,8 @@ export { default as React } from "react";
3
3
  export type { Dispatch, MutableRefObject, SetStateAction } from "react";
4
4
  export { ClientOnly } from "./ClientOnly.js";
5
5
  export { initClientNavigation, navigate } from "./navigation.js";
6
- import type { HydrationOptions, Transport } from "./types";
6
+ export type { ActionResponseData } from "./types";
7
+ import type { ActionResponseData, HydrationOptions, Transport } from "./types";
7
8
  export declare const fetchTransport: Transport;
8
9
  /**
9
10
  * Initializes the React client and hydrates the RSC payload.
@@ -12,8 +13,16 @@ export declare const fetchTransport: Transport;
12
13
  * making the page interactive. Call this from your client entry point.
13
14
  *
14
15
  * @param transport - Custom transport for server communication (defaults to fetchTransport)
15
- * @param hydrateRootOptions - Options passed to React's hydrateRoot
16
- * @param handleResponse - Custom response handler for navigation errors
16
+ * @param hydrateRootOptions - Options passed to React's `hydrateRoot`. Supports all React hydration options including:
17
+ * - `onUncaughtError`: Handler for uncaught errors (async errors, event handler errors).
18
+ * If not provided, defaults to logging errors to console.
19
+ * - `onCaughtError`: Handler for errors caught by error boundaries
20
+ * - `onRecoverableError`: Handler for recoverable errors
21
+ * @param handleResponse - Custom response handler for navigation errors (navigation GETs)
22
+ * @param onHydrationUpdate - Callback invoked after a new RSC payload has been committed on the client
23
+ * @param onActionResponse - Optional hook invoked when an action returns a Response;
24
+ * return true to signal that the response has been handled and
25
+ * default behaviour (e.g. redirects) should be skipped
17
26
  *
18
27
  * @example
19
28
  * // Basic usage
@@ -29,6 +38,23 @@ export declare const fetchTransport: Transport;
29
38
  * initClient({ handleResponse });
30
39
  *
31
40
  * @example
41
+ * // With error handling
42
+ * initClient({
43
+ * hydrateRootOptions: {
44
+ * onUncaughtError: (error, errorInfo) => {
45
+ * console.error("Uncaught error:", error);
46
+ * // Send to monitoring service
47
+ * sendToSentry(error, errorInfo);
48
+ * },
49
+ * onCaughtError: (error, errorInfo) => {
50
+ * console.error("Caught error:", error);
51
+ * // Handle errors from error boundaries
52
+ * sendToSentry(error, errorInfo);
53
+ * },
54
+ * },
55
+ * });
56
+ *
57
+ * @example
32
58
  * // With custom React hydration options
33
59
  * initClient({
34
60
  * hydrateRootOptions: {
@@ -38,8 +64,10 @@ export declare const fetchTransport: Transport;
38
64
  * },
39
65
  * });
40
66
  */
41
- export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, }?: {
67
+ export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, onHydrationUpdate, onActionResponse, }?: {
42
68
  transport?: Transport;
43
69
  hydrateRootOptions?: HydrationOptions;
44
70
  handleResponse?: (response: Response) => boolean;
71
+ onHydrationUpdate?: () => void;
72
+ onActionResponse?: (actionResponse: ActionResponseData) => boolean | void;
45
73
  }) => Promise<void>;
@@ -13,18 +13,38 @@ import { rscStream } from "rsc-html-stream/client";
13
13
  export { default as React } from "react";
14
14
  export { ClientOnly } from "./ClientOnly.js";
15
15
  export { initClientNavigation, navigate } from "./navigation.js";
16
+ import { getCachedNavigationResponse } from "./navigationCache.js";
17
+ import { isActionResponse } from "./types";
16
18
  export const fetchTransport = (transportContext) => {
17
- const fetchCallServer = async (id, args) => {
19
+ const fetchCallServer = async (id, args, source = "action") => {
18
20
  const url = new URL(window.location.href);
19
21
  url.searchParams.set("__rsc", "");
20
- if (id != null) {
22
+ const isAction = id != null;
23
+ if (isAction) {
21
24
  url.searchParams.set("__rsc_action_id", id);
22
25
  }
23
- const fetchPromise = fetch(url, {
24
- method: "POST",
25
- redirect: "manual",
26
- body: args != null ? await encodeReply(args) : null,
27
- });
26
+ let fetchPromise;
27
+ if (!isAction && source === "navigation") {
28
+ // Try to get cached response first
29
+ const cachedResponse = await getCachedNavigationResponse(url);
30
+ if (cachedResponse) {
31
+ fetchPromise = Promise.resolve(cachedResponse);
32
+ }
33
+ else {
34
+ // Fall back to network fetch on cache miss
35
+ fetchPromise = fetch(url, {
36
+ method: "GET",
37
+ redirect: "manual",
38
+ });
39
+ }
40
+ }
41
+ else {
42
+ fetchPromise = fetch(url, {
43
+ method: "POST",
44
+ redirect: "manual",
45
+ body: args != null ? await encodeReply(args) : null,
46
+ });
47
+ }
28
48
  // If there's a response handler, check the response first
29
49
  if (transportContext.handleResponse) {
30
50
  const response = await fetchPromise;
@@ -38,7 +58,21 @@ export const fetchTransport = (transportContext) => {
38
58
  });
39
59
  transportContext.setRscPayload(streamData);
40
60
  const result = await streamData;
41
- return result.actionResult;
61
+ const rawActionResult = result.actionResult;
62
+ if (isActionResponse(rawActionResult)) {
63
+ const actionResponse = rawActionResult.__rw_action_response;
64
+ const handledByHook = transportContext.onActionResponse?.(actionResponse) === true;
65
+ if (!handledByHook) {
66
+ const location = actionResponse.headers["location"];
67
+ const isRedirect = actionResponse.status >= 300 && actionResponse.status < 400;
68
+ if (location && isRedirect) {
69
+ window.location.href = location;
70
+ return undefined;
71
+ }
72
+ }
73
+ return rawActionResult;
74
+ }
75
+ return rawActionResult;
42
76
  }
43
77
  // Original behavior when no handler is present
44
78
  const streamData = createFromFetch(fetchPromise, {
@@ -46,7 +80,21 @@ export const fetchTransport = (transportContext) => {
46
80
  });
47
81
  transportContext.setRscPayload(streamData);
48
82
  const result = await streamData;
49
- return result.actionResult;
83
+ const rawActionResult = result.actionResult;
84
+ if (isActionResponse(rawActionResult)) {
85
+ const actionResponse = rawActionResult.__rw_action_response;
86
+ const handledByHook = transportContext.onActionResponse?.(actionResponse) === true;
87
+ if (!handledByHook) {
88
+ const location = actionResponse.headers["location"];
89
+ const isRedirect = actionResponse.status >= 300 && actionResponse.status < 400;
90
+ if (location && isRedirect) {
91
+ window.location.href = location;
92
+ return undefined;
93
+ }
94
+ }
95
+ return rawActionResult;
96
+ }
97
+ return rawActionResult;
50
98
  };
51
99
  return fetchCallServer;
52
100
  };
@@ -57,8 +105,16 @@ export const fetchTransport = (transportContext) => {
57
105
  * making the page interactive. Call this from your client entry point.
58
106
  *
59
107
  * @param transport - Custom transport for server communication (defaults to fetchTransport)
60
- * @param hydrateRootOptions - Options passed to React's hydrateRoot
61
- * @param handleResponse - Custom response handler for navigation errors
108
+ * @param hydrateRootOptions - Options passed to React's `hydrateRoot`. Supports all React hydration options including:
109
+ * - `onUncaughtError`: Handler for uncaught errors (async errors, event handler errors).
110
+ * If not provided, defaults to logging errors to console.
111
+ * - `onCaughtError`: Handler for errors caught by error boundaries
112
+ * - `onRecoverableError`: Handler for recoverable errors
113
+ * @param handleResponse - Custom response handler for navigation errors (navigation GETs)
114
+ * @param onHydrationUpdate - Callback invoked after a new RSC payload has been committed on the client
115
+ * @param onActionResponse - Optional hook invoked when an action returns a Response;
116
+ * return true to signal that the response has been handled and
117
+ * default behaviour (e.g. redirects) should be skipped
62
118
  *
63
119
  * @example
64
120
  * // Basic usage
@@ -74,6 +130,23 @@ export const fetchTransport = (transportContext) => {
74
130
  * initClient({ handleResponse });
75
131
  *
76
132
  * @example
133
+ * // With error handling
134
+ * initClient({
135
+ * hydrateRootOptions: {
136
+ * onUncaughtError: (error, errorInfo) => {
137
+ * console.error("Uncaught error:", error);
138
+ * // Send to monitoring service
139
+ * sendToSentry(error, errorInfo);
140
+ * },
141
+ * onCaughtError: (error, errorInfo) => {
142
+ * console.error("Caught error:", error);
143
+ * // Handle errors from error boundaries
144
+ * sendToSentry(error, errorInfo);
145
+ * },
146
+ * },
147
+ * });
148
+ *
149
+ * @example
77
150
  * // With custom React hydration options
78
151
  * initClient({
79
152
  * hydrateRootOptions: {
@@ -83,13 +156,17 @@ export const fetchTransport = (transportContext) => {
83
156
  * },
84
157
  * });
85
158
  */
86
- export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, } = {}) => {
159
+ export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, onHydrationUpdate, onActionResponse, } = {}) => {
87
160
  const transportContext = {
88
161
  setRscPayload: () => { },
89
162
  handleResponse,
163
+ onHydrationUpdate,
164
+ onActionResponse,
90
165
  };
91
166
  let transportCallServer = transport(transportContext);
92
- const callServer = (id, args) => transportCallServer(id, args);
167
+ const callServer = (id, args, source) => {
168
+ return transportCallServer(id, args, source);
169
+ };
93
170
  const upgradeToRealtime = async ({ key } = {}) => {
94
171
  const { realtimeTransport } = await import("../lib/realtime/client");
95
172
  const createRealtimeTransport = realtimeTransport({ key });
@@ -115,7 +192,14 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
115
192
  function Content() {
116
193
  const [streamData, setStreamData] = React.useState(rscPayload);
117
194
  const [_isPending, startTransition] = React.useTransition();
118
- transportContext.setRscPayload = (v) => startTransition(() => setStreamData(v));
195
+ transportContext.setRscPayload = (v) => startTransition(() => {
196
+ setStreamData(v);
197
+ });
198
+ React.useEffect(() => {
199
+ if (!streamData)
200
+ return;
201
+ transportContext.onHydrationUpdate?.();
202
+ }, [streamData]);
119
203
  return (_jsx(_Fragment, { children: streamData
120
204
  ? React.use(streamData).node
121
205
  : null }));
@@ -1,7 +1,10 @@
1
+ import { type NavigationCache, type NavigationCacheStorage } from "./navigationCache.js";
2
+ export type { NavigationCache, NavigationCacheStorage };
1
3
  export interface ClientNavigationOptions {
2
4
  onNavigate?: () => void;
3
5
  scrollToTop?: boolean;
4
6
  scrollBehavior?: "auto" | "smooth" | "instant";
7
+ cacheStorage?: NavigationCacheStorage;
5
8
  }
6
9
  export declare function validateClickEvent(event: MouseEvent, target: HTMLElement): boolean;
7
10
  export interface NavigateOptions {
@@ -26,8 +29,8 @@ export declare function navigate(href: string, options?: NavigateOptions): Promi
26
29
  * // Basic usage
27
30
  * import { initClient, initClientNavigation } from "rwsdk/client";
28
31
  *
29
- * const { handleResponse } = initClientNavigation();
30
- * initClient({ handleResponse });
32
+ * const { handleResponse, onHydrationUpdate } = initClientNavigation();
33
+ * initClient({ handleResponse, onHydrationUpdate });
31
34
  *
32
35
  * @example
33
36
  * // With custom scroll behavior
@@ -55,4 +58,5 @@ export declare function navigate(href: string, options?: NavigateOptions): Promi
55
58
  */
56
59
  export declare function initClientNavigation(opts?: ClientNavigationOptions): {
57
60
  handleResponse: (response: Response) => boolean;
61
+ onHydrationUpdate: () => void;
58
62
  };
@@ -1,3 +1,4 @@
1
+ import { onNavigationCommit, preloadFromLinkTags, } from "./navigationCache.js";
1
2
  export function validateClickEvent(event, target) {
2
3
  // should this only work for left click?
3
4
  if (event.button !== 0) {
@@ -44,8 +45,7 @@ export async function navigate(href, options = { history: "push" }) {
44
45
  else {
45
46
  window.history.replaceState({ path: href }, "", url);
46
47
  }
47
- // @ts-expect-error
48
- await globalThis.__rsc_callServer();
48
+ await globalThis.__rsc_callServer(null, null, "navigation");
49
49
  const scrollToTop = options.info?.scrollToTop ?? true;
50
50
  const scrollBehavior = options.info?.scrollBehavior ?? "instant";
51
51
  if (scrollToTop && history.scrollRestoration === "auto") {
@@ -78,8 +78,8 @@ function saveScrollPosition(x, y) {
78
78
  * // Basic usage
79
79
  * import { initClient, initClientNavigation } from "rwsdk/client";
80
80
  *
81
- * const { handleResponse } = initClientNavigation();
82
- * initClient({ handleResponse });
81
+ * const { handleResponse, onHydrationUpdate } = initClientNavigation();
82
+ * initClient({ handleResponse, onHydrationUpdate });
83
83
  *
84
84
  * @example
85
85
  * // With custom scroll behavior
@@ -116,22 +116,34 @@ export function initClientNavigation(opts = {}) {
116
116
  const el = event.target;
117
117
  const a = el.closest("a");
118
118
  const href = a?.getAttribute("href");
119
- navigate(href);
119
+ await navigate(href);
120
120
  }, true);
121
121
  window.addEventListener("popstate", async function handlePopState() {
122
- // @ts-expect-error
123
- await globalThis.__rsc_callServer();
122
+ await globalThis.__rsc_callServer(null, null, "navigation");
124
123
  });
125
- // Return a handleResponse function for use with initClient
124
+ function handleResponse(response) {
125
+ if (!response.ok) {
126
+ // Redirect to the current page (window.location) to show the error
127
+ // This means the page that produced the error is called twice.
128
+ window.location.href = window.location.href;
129
+ return false;
130
+ }
131
+ return true;
132
+ }
133
+ // Store cacheStorage globally for use in client.tsx
134
+ if (opts.cacheStorage && typeof globalThis !== "undefined") {
135
+ globalThis.__rsc_cacheStorage = opts.cacheStorage;
136
+ }
137
+ function onHydrationUpdate() {
138
+ // After each RSC hydration/update, increment generation and evict old caches,
139
+ // then warm the navigation cache based on any <link rel="prefetch"> tags
140
+ // rendered for the current location.
141
+ onNavigationCommit(undefined, opts.cacheStorage);
142
+ void preloadFromLinkTags(undefined, undefined, opts.cacheStorage);
143
+ }
144
+ // Return callbacks for use with initClient
126
145
  return {
127
- handleResponse: function handleResponse(response) {
128
- if (!response.ok) {
129
- // Redirect to the current page (window.location) to show the error
130
- // This means the page that produced the error is called twice.
131
- window.location.href = window.location.href;
132
- return false;
133
- }
134
- return true;
135
- },
146
+ handleResponse,
147
+ onHydrationUpdate,
136
148
  };
137
149
  }
@@ -0,0 +1,68 @@
1
+ export interface NavigationCacheEnvironment {
2
+ isSecureContext: boolean;
3
+ origin: string;
4
+ caches?: CacheStorage;
5
+ fetch: typeof fetch;
6
+ }
7
+ /**
8
+ * Interface for a single cache instance, mirroring the Cache API.
9
+ */
10
+ export interface NavigationCache {
11
+ put(request: Request, response: Response): Promise<void>;
12
+ match(request: Request): Promise<Response | undefined>;
13
+ }
14
+ /**
15
+ * Interface for cache storage, mirroring the CacheStorage API.
16
+ */
17
+ export interface NavigationCacheStorage {
18
+ open(cacheName: string): Promise<NavigationCache>;
19
+ delete(cacheName: string): Promise<boolean>;
20
+ keys(): Promise<string[]>;
21
+ }
22
+ /**
23
+ * Creates a default NavigationCacheStorage implementation that wraps the browser's CacheStorage API.
24
+ * This maintains the current generation-based cache naming and eviction logic.
25
+ */
26
+ export declare function createDefaultNavigationCacheStorage(env?: NavigationCacheEnvironment): NavigationCacheStorage | undefined;
27
+ /**
28
+ * Preloads the RSC navigation response for a given URL into the Cache API.
29
+ *
30
+ * This issues a GET request with the `__rsc` query parameter set, and, on a
31
+ * successful response, stores it in a versioned Cache using `cache.put`.
32
+ *
33
+ * See MDN for Cache interface semantics:
34
+ * https://developer.mozilla.org/en-US/docs/Web/API/Cache
35
+ */
36
+ export declare function preloadNavigationUrl(rawUrl: URL | string, env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): Promise<void>;
37
+ /**
38
+ * Attempts to retrieve a cached navigation response for the given URL.
39
+ *
40
+ * Returns the cached Response if found, or undefined if not cached or if
41
+ * CacheStorage is unavailable.
42
+ */
43
+ export declare function getCachedNavigationResponse(rawUrl: URL | string, env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): Promise<Response | undefined>;
44
+ /**
45
+ * Cleans up old generation caches for the current tab.
46
+ *
47
+ * This should be called after navigation commits to evict cache entries from
48
+ * previous navigations. It runs asynchronously via requestIdleCallback or
49
+ * setTimeout to avoid blocking the critical path.
50
+ */
51
+ export declare function evictOldGenerationCaches(env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): Promise<void>;
52
+ /**
53
+ * Increments the generation counter and schedules cleanup of old caches.
54
+ *
55
+ * This should be called after navigation commits to mark the current generation
56
+ * as complete and prepare for the next navigation cycle.
57
+ */
58
+ export declare function onNavigationCommit(env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): void;
59
+ /**
60
+ * Scan the document for `<link rel="prefetch" href="...">` elements that point
61
+ * to same-origin paths and prefetch their RSC navigation responses into the
62
+ * Cache API.
63
+ *
64
+ * This is invoked after client navigations to warm the navigation cache in
65
+ * the background. We intentionally keep Cache usage write-only for now; reads
66
+ * still go through the normal fetch path.
67
+ */
68
+ export declare function preloadFromLinkTags(doc?: Document, env?: NavigationCacheEnvironment, cacheStorage?: NavigationCacheStorage): Promise<void>;