rwsdk 1.0.0-beta.41 → 1.0.0-beta.42
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/lib/e2e/dev.mjs +14 -10
- package/dist/lib/e2e/index.d.mts +1 -0
- package/dist/lib/e2e/index.mjs +1 -0
- package/dist/lib/e2e/release.mjs +9 -4
- package/dist/lib/e2e/testHarness.d.mts +9 -4
- package/dist/lib/e2e/testHarness.mjs +20 -2
- package/dist/runtime/client/client.d.ts +10 -3
- package/dist/runtime/client/client.js +76 -13
- package/dist/runtime/client/navigation.d.ts +6 -2
- package/dist/runtime/client/navigation.js +29 -17
- package/dist/runtime/client/navigationCache.d.ts +68 -0
- package/dist/runtime/client/navigationCache.js +294 -0
- package/dist/runtime/client/navigationCache.test.d.ts +1 -0
- package/dist/runtime/client/navigationCache.test.js +456 -0
- package/dist/runtime/client/types.d.ts +25 -3
- package/dist/runtime/client/types.js +7 -1
- package/dist/runtime/lib/realtime/client.js +17 -1
- package/dist/runtime/render/normalizeActionResult.js +8 -1
- package/package.json +2 -1
package/dist/lib/e2e/dev.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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,
|
package/dist/lib/e2e/index.d.mts
CHANGED
package/dist/lib/e2e/index.mjs
CHANGED
package/dist/lib/e2e/release.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
@@ -13,7 +14,11 @@ export declare const fetchTransport: Transport;
|
|
|
13
14
|
*
|
|
14
15
|
* @param transport - Custom transport for server communication (defaults to fetchTransport)
|
|
15
16
|
* @param hydrateRootOptions - Options passed to React's hydrateRoot
|
|
16
|
-
* @param handleResponse - Custom response handler for navigation errors
|
|
17
|
+
* @param handleResponse - Custom response handler for navigation errors (navigation GETs)
|
|
18
|
+
* @param onHydrationUpdate - Callback invoked after a new RSC payload has been committed on the client
|
|
19
|
+
* @param onActionResponse - Optional hook invoked when an action returns a Response;
|
|
20
|
+
* return true to signal that the response has been handled and
|
|
21
|
+
* default behaviour (e.g. redirects) should be skipped
|
|
17
22
|
*
|
|
18
23
|
* @example
|
|
19
24
|
* // Basic usage
|
|
@@ -38,8 +43,10 @@ export declare const fetchTransport: Transport;
|
|
|
38
43
|
* },
|
|
39
44
|
* });
|
|
40
45
|
*/
|
|
41
|
-
export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, }?: {
|
|
46
|
+
export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, onHydrationUpdate, onActionResponse, }?: {
|
|
42
47
|
transport?: Transport;
|
|
43
48
|
hydrateRootOptions?: HydrationOptions;
|
|
44
49
|
handleResponse?: (response: Response) => boolean;
|
|
50
|
+
onHydrationUpdate?: () => void;
|
|
51
|
+
onActionResponse?: (actionResponse: ActionResponseData) => boolean | void;
|
|
45
52
|
}) => 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
|
-
|
|
22
|
+
const isAction = id != null;
|
|
23
|
+
if (isAction) {
|
|
21
24
|
url.searchParams.set("__rsc_action_id", id);
|
|
22
25
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
|
@@ -58,7 +106,11 @@ export const fetchTransport = (transportContext) => {
|
|
|
58
106
|
*
|
|
59
107
|
* @param transport - Custom transport for server communication (defaults to fetchTransport)
|
|
60
108
|
* @param hydrateRootOptions - Options passed to React's hydrateRoot
|
|
61
|
-
* @param handleResponse - Custom response handler for navigation errors
|
|
109
|
+
* @param handleResponse - Custom response handler for navigation errors (navigation GETs)
|
|
110
|
+
* @param onHydrationUpdate - Callback invoked after a new RSC payload has been committed on the client
|
|
111
|
+
* @param onActionResponse - Optional hook invoked when an action returns a Response;
|
|
112
|
+
* return true to signal that the response has been handled and
|
|
113
|
+
* default behaviour (e.g. redirects) should be skipped
|
|
62
114
|
*
|
|
63
115
|
* @example
|
|
64
116
|
* // Basic usage
|
|
@@ -83,13 +135,17 @@ export const fetchTransport = (transportContext) => {
|
|
|
83
135
|
* },
|
|
84
136
|
* });
|
|
85
137
|
*/
|
|
86
|
-
export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, } = {}) => {
|
|
138
|
+
export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, onHydrationUpdate, onActionResponse, } = {}) => {
|
|
87
139
|
const transportContext = {
|
|
88
140
|
setRscPayload: () => { },
|
|
89
141
|
handleResponse,
|
|
142
|
+
onHydrationUpdate,
|
|
143
|
+
onActionResponse,
|
|
90
144
|
};
|
|
91
145
|
let transportCallServer = transport(transportContext);
|
|
92
|
-
const callServer = (id, args) =>
|
|
146
|
+
const callServer = (id, args, source) => {
|
|
147
|
+
return transportCallServer(id, args, source);
|
|
148
|
+
};
|
|
93
149
|
const upgradeToRealtime = async ({ key } = {}) => {
|
|
94
150
|
const { realtimeTransport } = await import("../lib/realtime/client");
|
|
95
151
|
const createRealtimeTransport = realtimeTransport({ key });
|
|
@@ -115,7 +171,14 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
|
|
|
115
171
|
function Content() {
|
|
116
172
|
const [streamData, setStreamData] = React.useState(rscPayload);
|
|
117
173
|
const [_isPending, startTransition] = React.useTransition();
|
|
118
|
-
transportContext.setRscPayload = (v) => startTransition(() =>
|
|
174
|
+
transportContext.setRscPayload = (v) => startTransition(() => {
|
|
175
|
+
setStreamData(v);
|
|
176
|
+
});
|
|
177
|
+
React.useEffect(() => {
|
|
178
|
+
if (!streamData)
|
|
179
|
+
return;
|
|
180
|
+
transportContext.onHydrationUpdate?.();
|
|
181
|
+
}, [streamData]);
|
|
119
182
|
return (_jsx(_Fragment, { children: streamData
|
|
120
183
|
? React.use(streamData).node
|
|
121
184
|
: 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
|
-
|
|
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
|
-
|
|
123
|
-
await globalThis.__rsc_callServer();
|
|
122
|
+
await globalThis.__rsc_callServer(null, null, "navigation");
|
|
124
123
|
});
|
|
125
|
-
|
|
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
|
|
128
|
-
|
|
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>;
|