rwsdk 1.0.0-beta.5 → 1.0.0-beta.51

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 (157) hide show
  1. package/bin/rw-scripts.mjs +13 -13
  2. package/dist/lib/constants.d.mts +1 -0
  3. package/dist/lib/constants.mjs +7 -4
  4. package/dist/lib/e2e/browser.mjs +6 -2
  5. package/dist/lib/e2e/constants.d.mts +4 -0
  6. package/dist/lib/e2e/constants.mjs +49 -12
  7. package/dist/lib/e2e/dev.mjs +49 -57
  8. package/dist/lib/e2e/environment.d.mts +2 -0
  9. package/dist/lib/e2e/environment.mjs +201 -64
  10. package/dist/lib/e2e/index.d.mts +2 -0
  11. package/dist/lib/e2e/index.mjs +2 -0
  12. package/dist/lib/e2e/poll.d.mts +1 -1
  13. package/dist/lib/e2e/release.d.mts +1 -0
  14. package/dist/lib/e2e/release.mjs +57 -52
  15. package/dist/lib/e2e/tarball.mjs +2 -34
  16. package/dist/lib/e2e/testHarness.d.mts +39 -3
  17. package/dist/lib/e2e/testHarness.mjs +239 -92
  18. package/dist/lib/e2e/utils.d.mts +1 -0
  19. package/dist/lib/e2e/utils.mjs +15 -0
  20. package/dist/lib/normalizeModulePath.mjs +1 -1
  21. package/dist/runtime/client/client.d.ts +64 -2
  22. package/dist/runtime/client/client.js +156 -15
  23. package/dist/runtime/client/navigation.d.ts +45 -0
  24. package/dist/runtime/client/navigation.js +68 -14
  25. package/dist/runtime/client/navigationCache.d.ts +68 -0
  26. package/dist/runtime/client/navigationCache.js +294 -0
  27. package/dist/runtime/client/navigationCache.test.js +469 -0
  28. package/dist/runtime/client/types.d.ts +26 -5
  29. package/dist/runtime/client/types.js +8 -1
  30. package/dist/runtime/entries/no-react-server-ssr-bridge.d.ts +0 -0
  31. package/dist/runtime/entries/no-react-server-ssr-bridge.js +2 -0
  32. package/dist/runtime/entries/no-react-server.js +3 -1
  33. package/dist/runtime/entries/react-server-only.js +1 -1
  34. package/dist/runtime/entries/router.d.ts +1 -0
  35. package/dist/runtime/entries/routerClient.d.ts +1 -0
  36. package/dist/runtime/entries/routerClient.js +1 -0
  37. package/dist/runtime/entries/worker.d.ts +4 -0
  38. package/dist/runtime/entries/worker.js +4 -0
  39. package/dist/runtime/imports/__mocks__/use-client-lookup.d.ts +6 -0
  40. package/dist/runtime/imports/__mocks__/use-client-lookup.js +6 -0
  41. package/dist/runtime/lib/db/SqliteDurableObject.d.ts +2 -2
  42. package/dist/runtime/lib/db/SqliteDurableObject.js +2 -2
  43. package/dist/runtime/lib/db/createDb.d.ts +1 -2
  44. package/dist/runtime/lib/db/createDb.js +4 -0
  45. package/dist/runtime/lib/db/typeInference/builders/alterTable.d.ts +13 -3
  46. package/dist/runtime/lib/db/typeInference/builders/columnDefinition.d.ts +35 -21
  47. package/dist/runtime/lib/db/typeInference/builders/createTable.d.ts +9 -2
  48. package/dist/runtime/lib/db/typeInference/database.d.ts +16 -2
  49. package/dist/runtime/lib/db/typeInference/typetests/alterTable.typetest.js +80 -5
  50. package/dist/runtime/lib/db/typeInference/typetests/createTable.typetest.js +104 -2
  51. package/dist/runtime/lib/db/typeInference/typetests/testUtils.d.ts +1 -0
  52. package/dist/runtime/lib/db/typeInference/utils.d.ts +59 -9
  53. package/dist/runtime/lib/links.d.ts +21 -7
  54. package/dist/runtime/lib/links.js +84 -26
  55. package/dist/runtime/lib/links.test.d.ts +1 -0
  56. package/dist/runtime/lib/links.test.js +20 -0
  57. package/dist/runtime/lib/manifest.d.ts +1 -1
  58. package/dist/runtime/lib/manifest.js +7 -4
  59. package/dist/runtime/lib/realtime/client.js +28 -6
  60. package/dist/runtime/lib/realtime/worker.d.ts +1 -1
  61. package/dist/runtime/lib/router.d.ts +154 -35
  62. package/dist/runtime/lib/router.js +491 -105
  63. package/dist/runtime/lib/router.test.js +611 -1
  64. package/dist/runtime/lib/stitchDocumentAndAppStreams.d.ts +66 -0
  65. package/dist/runtime/lib/stitchDocumentAndAppStreams.js +302 -35
  66. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.d.ts +1 -0
  67. package/dist/runtime/lib/stitchDocumentAndAppStreams.test.js +418 -0
  68. package/dist/runtime/lib/{rwContext.d.ts → types.d.ts} +1 -0
  69. package/dist/runtime/lib/types.js +1 -0
  70. package/dist/runtime/register/client.d.ts +1 -1
  71. package/dist/runtime/register/client.js +10 -3
  72. package/dist/runtime/register/worker.js +13 -4
  73. package/dist/runtime/render/normalizeActionResult.js +8 -1
  74. package/dist/runtime/render/renderDocumentHtmlStream.d.ts +1 -1
  75. package/dist/runtime/render/renderToStream.d.ts +4 -2
  76. package/dist/runtime/render/renderToStream.js +53 -24
  77. package/dist/runtime/render/renderToString.d.ts +3 -6
  78. package/dist/runtime/requestInfo/types.d.ts +5 -1
  79. package/dist/runtime/requestInfo/utils.d.ts +9 -0
  80. package/dist/runtime/requestInfo/utils.js +45 -0
  81. package/dist/runtime/requestInfo/worker.d.ts +0 -1
  82. package/dist/runtime/requestInfo/worker.js +5 -11
  83. package/dist/runtime/script.d.ts +1 -3
  84. package/dist/runtime/script.js +1 -10
  85. package/dist/runtime/server.d.ts +52 -0
  86. package/dist/runtime/server.js +88 -0
  87. package/dist/runtime/state.d.ts +3 -0
  88. package/dist/runtime/state.js +13 -0
  89. package/dist/runtime/worker.d.ts +3 -1
  90. package/dist/runtime/worker.js +45 -2
  91. package/dist/scripts/debug-sync.mjs +18 -20
  92. package/dist/scripts/worker-run.d.mts +1 -1
  93. package/dist/scripts/worker-run.mjs +59 -113
  94. package/dist/use-synced-state/SyncedStateServer.d.mts +36 -0
  95. package/dist/use-synced-state/SyncedStateServer.mjs +196 -0
  96. package/dist/use-synced-state/__tests__/SyncStateServer.test.d.mts +1 -0
  97. package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +116 -0
  98. package/dist/use-synced-state/__tests__/useSyncState.test.d.ts +1 -0
  99. package/dist/use-synced-state/__tests__/useSyncState.test.js +115 -0
  100. package/dist/use-synced-state/__tests__/useSyncedState.test.d.ts +1 -0
  101. package/dist/use-synced-state/__tests__/useSyncedState.test.js +115 -0
  102. package/dist/use-synced-state/__tests__/worker.test.d.mts +1 -0
  103. package/dist/use-synced-state/__tests__/worker.test.mjs +70 -0
  104. package/dist/use-synced-state/client-core.d.ts +29 -0
  105. package/dist/use-synced-state/client-core.js +103 -0
  106. package/dist/use-synced-state/client.d.ts +3 -0
  107. package/dist/use-synced-state/client.js +4 -0
  108. package/dist/use-synced-state/constants.d.mts +1 -0
  109. package/dist/use-synced-state/constants.mjs +1 -0
  110. package/dist/use-synced-state/useSyncedState.d.ts +21 -0
  111. package/dist/use-synced-state/useSyncedState.js +64 -0
  112. package/dist/use-synced-state/worker.d.mts +14 -0
  113. package/dist/use-synced-state/worker.mjs +135 -0
  114. package/dist/vite/buildApp.mjs +34 -2
  115. package/dist/vite/cloudflarePreInitPlugin.d.mts +11 -0
  116. package/dist/vite/cloudflarePreInitPlugin.mjs +40 -0
  117. package/dist/vite/configPlugin.mjs +9 -14
  118. package/dist/vite/constants.d.mts +1 -0
  119. package/dist/vite/constants.mjs +1 -0
  120. package/dist/vite/createDirectiveLookupPlugin.mjs +10 -7
  121. package/dist/vite/devServerTimingPlugin.mjs +4 -0
  122. package/dist/vite/diagnosticAssetGraphPlugin.d.mts +4 -0
  123. package/dist/vite/diagnosticAssetGraphPlugin.mjs +41 -0
  124. package/dist/vite/directiveModulesDevPlugin.mjs +9 -1
  125. package/dist/vite/directivesPlugin.mjs +4 -4
  126. package/dist/vite/envResolvers.d.mts +11 -0
  127. package/dist/vite/envResolvers.mjs +20 -0
  128. package/dist/vite/getViteEsbuild.mjs +2 -1
  129. package/dist/vite/hmrStabilityPlugin.d.mts +2 -0
  130. package/dist/vite/hmrStabilityPlugin.mjs +73 -0
  131. package/dist/vite/injectVitePreamblePlugin.mjs +0 -4
  132. package/dist/vite/knownDepsResolverPlugin.d.mts +0 -6
  133. package/dist/vite/knownDepsResolverPlugin.mjs +25 -17
  134. package/dist/vite/linkerPlugin.d.mts +2 -1
  135. package/dist/vite/linkerPlugin.mjs +11 -3
  136. package/dist/vite/linkerPlugin.test.mjs +15 -0
  137. package/dist/vite/miniflareHMRPlugin.mjs +6 -38
  138. package/dist/vite/moveStaticAssetsPlugin.mjs +35 -4
  139. package/dist/vite/redwoodPlugin.mjs +9 -11
  140. package/dist/vite/redwoodPlugin.test.mjs +4 -4
  141. package/dist/vite/runDirectivesScan.mjs +75 -19
  142. package/dist/vite/ssrBridgePlugin.mjs +132 -40
  143. package/dist/vite/ssrBridgeWrapPlugin.d.mts +2 -0
  144. package/dist/vite/ssrBridgeWrapPlugin.mjs +85 -0
  145. package/dist/vite/staleDepRetryPlugin.d.mts +2 -0
  146. package/dist/vite/staleDepRetryPlugin.mjs +74 -0
  147. package/dist/vite/statePlugin.d.mts +4 -0
  148. package/dist/vite/statePlugin.mjs +62 -0
  149. package/dist/vite/transformClientComponents.test.mjs +32 -0
  150. package/dist/vite/transformJsxScriptTagsPlugin.mjs +0 -5
  151. package/dist/vite/transformServerFunctions.mjs +66 -4
  152. package/dist/vite/transformServerFunctions.test.mjs +35 -0
  153. package/dist/vite/virtualPlugin.mjs +6 -7
  154. package/package.json +45 -20
  155. package/dist/vite/manifestPlugin.d.mts +0 -4
  156. package/dist/vite/manifestPlugin.mjs +0 -63
  157. /package/dist/runtime/{lib/rwContext.js → client/navigationCache.test.d.ts} +0 -0
@@ -1,11 +1,73 @@
1
1
  import "./setWebpackRequire";
2
2
  export { default as React } from "react";
3
+ export type { Dispatch, MutableRefObject, SetStateAction } from "react";
3
4
  export { ClientOnly } from "./ClientOnly.js";
4
5
  export { initClientNavigation, navigate } from "./navigation.js";
5
- import type { HydrationOptions, Transport } from "./types";
6
+ export type { ActionResponseData } from "./types";
7
+ import type { ActionResponseData, HydrationOptions, Transport } from "./types";
6
8
  export declare const fetchTransport: Transport;
7
- export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, }?: {
9
+ /**
10
+ * Initializes the React client and hydrates the RSC payload.
11
+ *
12
+ * This function sets up client-side hydration for React Server Components,
13
+ * making the page interactive. Call this from your client entry point.
14
+ *
15
+ * @param transport - Custom transport for server communication (defaults to fetchTransport)
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 onHydrated - 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
26
+ *
27
+ * @example
28
+ * // Basic usage
29
+ * import { initClient } from "rwsdk/client";
30
+ *
31
+ * initClient();
32
+ *
33
+ * @example
34
+ * // With client-side navigation
35
+ * import { initClient, initClientNavigation } from "rwsdk/client";
36
+ *
37
+ * const { handleResponse } = initClientNavigation();
38
+ * initClient({ handleResponse });
39
+ *
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
58
+ * // With custom React hydration options
59
+ * initClient({
60
+ * hydrateRootOptions: {
61
+ * onRecoverableError: (error) => {
62
+ * console.warn("Recoverable error:", error);
63
+ * },
64
+ * },
65
+ * });
66
+ */
67
+ export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, onHydrated, onActionResponse, }?: {
8
68
  transport?: Transport;
9
69
  hydrateRootOptions?: HydrationOptions;
10
70
  handleResponse?: (response: Response) => boolean;
71
+ onHydrated?: () => void;
72
+ onActionResponse?: (actionResponse: ActionResponseData) => boolean | void;
11
73
  }) => Promise<void>;
@@ -4,6 +4,7 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
4
4
  // context(justinvdm, 14 Aug 2025): `react-server-dom-webpack` uses this global
5
5
  // to load modules, so we need to define it here before importing
6
6
  // "react-server-dom-webpack."
7
+ // prettier-ignore
7
8
  import "./setWebpackRequire";
8
9
  import React from "react";
9
10
  import { hydrateRoot } from "react-dom/client";
@@ -12,18 +13,57 @@ import { rscStream } from "rsc-html-stream/client";
12
13
  export { default as React } from "react";
13
14
  export { ClientOnly } from "./ClientOnly.js";
14
15
  export { initClientNavigation, navigate } from "./navigation.js";
16
+ import { getCachedNavigationResponse } from "./navigationCache.js";
17
+ import { isActionResponse } from "./types";
15
18
  export const fetchTransport = (transportContext) => {
16
- const fetchCallServer = async (id, args) => {
19
+ const fetchCallServer = async (id, args, source = "action", method = "POST") => {
17
20
  const url = new URL(window.location.href);
18
21
  url.searchParams.set("__rsc", "");
19
- if (id != null) {
22
+ const isAction = id != null;
23
+ if (isAction) {
20
24
  url.searchParams.set("__rsc_action_id", id);
25
+ // If args are provided and method is GET, serialize them into the query string
26
+ if (args != null && method === "GET") {
27
+ url.searchParams.set("args", JSON.stringify(args));
28
+ }
29
+ }
30
+ let fetchPromise;
31
+ if (!isAction && source === "navigation") {
32
+ // Try to get cached response first
33
+ const cachedResponse = await getCachedNavigationResponse(url);
34
+ if (cachedResponse) {
35
+ fetchPromise = Promise.resolve(cachedResponse);
36
+ }
37
+ else {
38
+ // Fall back to network fetch on cache miss
39
+ fetchPromise = fetch(url, {
40
+ method: "GET",
41
+ redirect: "manual",
42
+ });
43
+ }
44
+ }
45
+ else {
46
+ const headers = new Headers();
47
+ // Add x-rsc-data-only header if we want to skip the React tree render on the server
48
+ if (source === "query") {
49
+ headers.set("x-rsc-data-only", "true");
50
+ }
51
+ if (method === "GET") {
52
+ fetchPromise = fetch(url, {
53
+ method: "GET",
54
+ headers,
55
+ redirect: "manual",
56
+ });
57
+ }
58
+ else {
59
+ fetchPromise = fetch(url, {
60
+ method: "POST",
61
+ headers,
62
+ redirect: "manual",
63
+ body: args != null ? await encodeReply(args) : null,
64
+ });
65
+ }
21
66
  }
22
- const fetchPromise = fetch(url, {
23
- method: "POST",
24
- redirect: "manual",
25
- body: args != null ? await encodeReply(args) : null,
26
- });
27
67
  // If there's a response handler, check the response first
28
68
  if (transportContext.handleResponse) {
29
69
  const response = await fetchPromise;
@@ -35,27 +75,121 @@ export const fetchTransport = (transportContext) => {
35
75
  const streamData = createFromFetch(Promise.resolve(response), {
36
76
  callServer: fetchCallServer,
37
77
  });
38
- transportContext.setRscPayload(streamData);
78
+ if (source === "navigation" || source === "action") {
79
+ transportContext.setRscPayload(streamData);
80
+ }
39
81
  const result = await streamData;
40
- return result.actionResult;
82
+ const rawActionResult = result.actionResult;
83
+ if (isActionResponse(rawActionResult)) {
84
+ const actionResponse = rawActionResult.__rw_action_response;
85
+ const handledByHook = transportContext.onActionResponse?.(actionResponse) === true;
86
+ if (!handledByHook) {
87
+ const location = actionResponse.headers["location"];
88
+ const isRedirect = actionResponse.status >= 300 && actionResponse.status < 400;
89
+ if (location && isRedirect) {
90
+ window.location.href = location;
91
+ return undefined;
92
+ }
93
+ }
94
+ return rawActionResult;
95
+ }
96
+ return rawActionResult;
41
97
  }
42
98
  // Original behavior when no handler is present
43
99
  const streamData = createFromFetch(fetchPromise, {
44
100
  callServer: fetchCallServer,
45
101
  });
46
- transportContext.setRscPayload(streamData);
102
+ if (source === "navigation" || source === "action") {
103
+ transportContext.setRscPayload(streamData);
104
+ }
47
105
  const result = await streamData;
48
- return result.actionResult;
106
+ const rawActionResult = result.actionResult;
107
+ if (isActionResponse(rawActionResult)) {
108
+ const actionResponse = rawActionResult.__rw_action_response;
109
+ const handledByHook = transportContext.onActionResponse?.(actionResponse) === true;
110
+ if (!handledByHook) {
111
+ const location = actionResponse.headers["location"];
112
+ const isRedirect = actionResponse.status >= 300 && actionResponse.status < 400;
113
+ if (location && isRedirect) {
114
+ window.location.href = location;
115
+ return undefined;
116
+ }
117
+ }
118
+ return rawActionResult;
119
+ }
120
+ return rawActionResult;
49
121
  };
50
122
  return fetchCallServer;
51
123
  };
52
- export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, } = {}) => {
124
+ /**
125
+ * Initializes the React client and hydrates the RSC payload.
126
+ *
127
+ * This function sets up client-side hydration for React Server Components,
128
+ * making the page interactive. Call this from your client entry point.
129
+ *
130
+ * @param transport - Custom transport for server communication (defaults to fetchTransport)
131
+ * @param hydrateRootOptions - Options passed to React's `hydrateRoot`. Supports all React hydration options including:
132
+ * - `onUncaughtError`: Handler for uncaught errors (async errors, event handler errors).
133
+ * If not provided, defaults to logging errors to console.
134
+ * - `onCaughtError`: Handler for errors caught by error boundaries
135
+ * - `onRecoverableError`: Handler for recoverable errors
136
+ * @param handleResponse - Custom response handler for navigation errors (navigation GETs)
137
+ * @param onHydrated - Callback invoked after a new RSC payload has been committed on the client
138
+ * @param onActionResponse - Optional hook invoked when an action returns a Response;
139
+ * return true to signal that the response has been handled and
140
+ * default behaviour (e.g. redirects) should be skipped
141
+ *
142
+ * @example
143
+ * // Basic usage
144
+ * import { initClient } from "rwsdk/client";
145
+ *
146
+ * initClient();
147
+ *
148
+ * @example
149
+ * // With client-side navigation
150
+ * import { initClient, initClientNavigation } from "rwsdk/client";
151
+ *
152
+ * const { handleResponse } = initClientNavigation();
153
+ * initClient({ handleResponse });
154
+ *
155
+ * @example
156
+ * // With error handling
157
+ * initClient({
158
+ * hydrateRootOptions: {
159
+ * onUncaughtError: (error, errorInfo) => {
160
+ * console.error("Uncaught error:", error);
161
+ * // Send to monitoring service
162
+ * sendToSentry(error, errorInfo);
163
+ * },
164
+ * onCaughtError: (error, errorInfo) => {
165
+ * console.error("Caught error:", error);
166
+ * // Handle errors from error boundaries
167
+ * sendToSentry(error, errorInfo);
168
+ * },
169
+ * },
170
+ * });
171
+ *
172
+ * @example
173
+ * // With custom React hydration options
174
+ * initClient({
175
+ * hydrateRootOptions: {
176
+ * onRecoverableError: (error) => {
177
+ * console.warn("Recoverable error:", error);
178
+ * },
179
+ * },
180
+ * });
181
+ */
182
+ export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, onHydrated, onActionResponse, } = {}) => {
53
183
  const transportContext = {
54
184
  setRscPayload: () => { },
55
185
  handleResponse,
186
+ onHydrated,
187
+ onActionResponse,
56
188
  };
57
189
  let transportCallServer = transport(transportContext);
58
- const callServer = (id, args) => transportCallServer(id, args);
190
+ const callServer = (id, args, source, method) => {
191
+ return transportCallServer(id, args, source, method);
192
+ };
59
193
  const upgradeToRealtime = async ({ key } = {}) => {
60
194
  const { realtimeTransport } = await import("../lib/realtime/client");
61
195
  const createRealtimeTransport = realtimeTransport({ key });
@@ -68,7 +202,7 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
68
202
  };
69
203
  const rootEl = document.getElementById("hydrate-root");
70
204
  if (!rootEl) {
71
- throw new Error('no element with id "hydrate-root"');
205
+ throw new Error('RedwoodSDK: No element with id "hydrate-root" found in the document. This element is required for hydration. Ensure your Document component contains a <div id="hydrate-root">{children}</div>.');
72
206
  }
73
207
  let rscPayload;
74
208
  // context(justinvdm, 18 Jun 2025): We inject the RSC payload
@@ -81,7 +215,14 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
81
215
  function Content() {
82
216
  const [streamData, setStreamData] = React.useState(rscPayload);
83
217
  const [_isPending, startTransition] = React.useTransition();
84
- transportContext.setRscPayload = (v) => startTransition(() => setStreamData(v));
218
+ transportContext.setRscPayload = (v) => startTransition(() => {
219
+ setStreamData(v);
220
+ });
221
+ React.useEffect(() => {
222
+ if (!streamData)
223
+ return;
224
+ transportContext.onHydrated?.();
225
+ }, [streamData]);
85
226
  return (_jsx(_Fragment, { children: streamData
86
227
  ? React.use(streamData).node
87
228
  : 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 {
@@ -12,6 +15,48 @@ export interface NavigateOptions {
12
15
  };
13
16
  }
14
17
  export declare function navigate(href: string, options?: NavigateOptions): Promise<void>;
18
+ /**
19
+ * Initializes client-side navigation for Single Page App (SPA) behavior.
20
+ *
21
+ * Intercepts clicks on internal links and fetches page content without full-page reloads.
22
+ * Returns a handleResponse function to pass to initClient.
23
+ *
24
+ * @param opts.scrollToTop - Scroll to top after navigation (default: true)
25
+ * @param opts.scrollBehavior - How to scroll: 'instant', 'smooth', or 'auto' (default: 'instant')
26
+ * @param opts.onNavigate - Callback executed after history push but before RSC fetch
27
+ *
28
+ * @example
29
+ * // Basic usage
30
+ * import { initClient, initClientNavigation } from "rwsdk/client";
31
+ *
32
+ * const { handleResponse, onHydrated } = initClientNavigation();
33
+ * initClient({ handleResponse, onHydrated });
34
+ *
35
+ * @example
36
+ * // With custom scroll behavior
37
+ * const { handleResponse } = initClientNavigation({
38
+ * scrollBehavior: "smooth",
39
+ * scrollToTop: true,
40
+ * });
41
+ * initClient({ handleResponse });
42
+ *
43
+ * @example
44
+ * // Preserve scroll position (e.g., for infinite scroll)
45
+ * const { handleResponse } = initClientNavigation({
46
+ * scrollToTop: false,
47
+ * });
48
+ * initClient({ handleResponse });
49
+ *
50
+ * @example
51
+ * // With navigation callback
52
+ * const { handleResponse } = initClientNavigation({
53
+ * onNavigate: () => {
54
+ * console.log("Navigating to:", window.location.href);
55
+ * },
56
+ * });
57
+ * initClient({ handleResponse });
58
+ */
15
59
  export declare function initClientNavigation(opts?: ClientNavigationOptions): {
16
60
  handleResponse: (response: Response) => boolean;
61
+ onHydrated: () => void;
17
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,7 +45,7 @@ export async function navigate(href, options = { history: "push" }) {
44
45
  else {
45
46
  window.history.replaceState({ path: href }, "", url);
46
47
  }
47
- await globalThis.__rsc_callServer;
48
+ await globalThis.__rsc_callServer(null, null, "navigation");
48
49
  const scrollToTop = options.info?.scrollToTop ?? true;
49
50
  const scrollBehavior = options.info?.scrollBehavior ?? "instant";
50
51
  if (scrollToTop && history.scrollRestoration === "auto") {
@@ -63,6 +64,47 @@ function saveScrollPosition(x, y) {
63
64
  scrollY: y,
64
65
  }, "", window.location.href);
65
66
  }
67
+ /**
68
+ * Initializes client-side navigation for Single Page App (SPA) behavior.
69
+ *
70
+ * Intercepts clicks on internal links and fetches page content without full-page reloads.
71
+ * Returns a handleResponse function to pass to initClient.
72
+ *
73
+ * @param opts.scrollToTop - Scroll to top after navigation (default: true)
74
+ * @param opts.scrollBehavior - How to scroll: 'instant', 'smooth', or 'auto' (default: 'instant')
75
+ * @param opts.onNavigate - Callback executed after history push but before RSC fetch
76
+ *
77
+ * @example
78
+ * // Basic usage
79
+ * import { initClient, initClientNavigation } from "rwsdk/client";
80
+ *
81
+ * const { handleResponse, onHydrated } = initClientNavigation();
82
+ * initClient({ handleResponse, onHydrated });
83
+ *
84
+ * @example
85
+ * // With custom scroll behavior
86
+ * const { handleResponse } = initClientNavigation({
87
+ * scrollBehavior: "smooth",
88
+ * scrollToTop: true,
89
+ * });
90
+ * initClient({ handleResponse });
91
+ *
92
+ * @example
93
+ * // Preserve scroll position (e.g., for infinite scroll)
94
+ * const { handleResponse } = initClientNavigation({
95
+ * scrollToTop: false,
96
+ * });
97
+ * initClient({ handleResponse });
98
+ *
99
+ * @example
100
+ * // With navigation callback
101
+ * const { handleResponse } = initClientNavigation({
102
+ * onNavigate: () => {
103
+ * console.log("Navigating to:", window.location.href);
104
+ * },
105
+ * });
106
+ * initClient({ handleResponse });
107
+ */
66
108
  export function initClientNavigation(opts = {}) {
67
109
  IS_CLIENT_NAVIGATION = true;
68
110
  history.scrollRestoration = "auto";
@@ -74,22 +116,34 @@ export function initClientNavigation(opts = {}) {
74
116
  const el = event.target;
75
117
  const a = el.closest("a");
76
118
  const href = a?.getAttribute("href");
77
- navigate(href);
119
+ await navigate(href);
78
120
  }, true);
79
121
  window.addEventListener("popstate", async function handlePopState() {
80
- // @ts-expect-error
81
- await globalThis.__rsc_callServer();
122
+ await globalThis.__rsc_callServer(null, null, "navigation");
82
123
  });
83
- // 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 onHydrated() {
138
+ // After each RSC hydration/update, increment generation and evict old caches,
139
+ // then warm the navigation cache based on any <link rel="x-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
84
145
  return {
85
- handleResponse: function handleResponse(response) {
86
- if (!response.ok) {
87
- // Redirect to the current page (window.location) to show the error
88
- // This means the page that produced the error is called twice.
89
- window.location.href = window.location.href;
90
- return false;
91
- }
92
- return true;
93
- },
146
+ handleResponse,
147
+ onHydrated,
94
148
  };
95
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="x-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>;