rwsdk 1.0.0-beta.54 → 1.0.0-beta.55

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.
@@ -64,22 +64,7 @@ export const fetchTransport = (transportContext) => {
64
64
  });
65
65
  }
66
66
  }
67
- // If there's a response handler, check the response first
68
- if (transportContext.handleResponse) {
69
- const response = await fetchPromise;
70
- const shouldContinue = transportContext.handleResponse(response);
71
- if (!shouldContinue) {
72
- return;
73
- }
74
- // Continue with the response if handler returned true
75
- const streamData = createFromFetch(Promise.resolve(response), {
76
- callServer: fetchCallServer,
77
- });
78
- if (source === "navigation" || source === "action") {
79
- transportContext.setRscPayload(streamData);
80
- }
81
- const result = await streamData;
82
- const rawActionResult = result.actionResult;
67
+ const processActionResponse = (rawActionResult) => {
83
68
  if (isActionResponse(rawActionResult)) {
84
69
  const actionResponse = rawActionResult.__rw_action_response;
85
70
  const handledByHook = transportContext.onActionResponse?.(actionResponse) === true;
@@ -90,34 +75,49 @@ export const fetchTransport = (transportContext) => {
90
75
  window.location.href = location;
91
76
  return undefined;
92
77
  }
78
+ if (actionResponse.status >= 400) {
79
+ throw new Error(`Server function returned status ${actionResponse.status}`);
80
+ }
93
81
  }
94
82
  return rawActionResult;
95
83
  }
96
84
  return rawActionResult;
85
+ };
86
+ // If there's a response handler, check the response first
87
+ if (transportContext.handleResponse) {
88
+ const response = await fetchPromise;
89
+ const shouldContinue = transportContext.handleResponse(response);
90
+ if (!shouldContinue) {
91
+ return undefined;
92
+ }
93
+ // Continue with the response if handler returned true
94
+ const streamData = createFromFetch(Promise.resolve(response), {
95
+ callServer: fetchCallServer,
96
+ });
97
+ if (source === "navigation" || source === "action") {
98
+ transportContext.setRscPayload(streamData);
99
+ }
100
+ const result = await streamData;
101
+ return processActionResponse(result.actionResult);
97
102
  }
98
103
  // Original behavior when no handler is present
99
- const streamData = createFromFetch(fetchPromise, {
104
+ const response = await fetchPromise;
105
+ console.log("[fetchTransport] Response status:", response.status);
106
+ const location = response.headers.get("Location");
107
+ console.log("[fetchTransport] Location header:", location);
108
+ if (response.status >= 300 && response.status < 400 && location) {
109
+ console.log("[fetchTransport] Redirecting to:", location);
110
+ window.location.href = location;
111
+ return undefined;
112
+ }
113
+ const streamData = createFromFetch(Promise.resolve(response), {
100
114
  callServer: fetchCallServer,
101
115
  });
102
116
  if (source === "navigation" || source === "action") {
103
117
  transportContext.setRscPayload(streamData);
104
118
  }
105
119
  const result = await streamData;
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;
120
+ return processActionResponse(result.actionResult);
121
121
  };
122
122
  return fetchCallServer;
123
123
  };
@@ -122,6 +122,13 @@ export function initClientNavigation(opts = {}) {
122
122
  await globalThis.__rsc_callServer(null, null, "navigation");
123
123
  });
124
124
  function handleResponse(response) {
125
+ if (response.status >= 300 && response.status < 400) {
126
+ const location = response.headers.get("Location");
127
+ if (location) {
128
+ window.location.href = location;
129
+ return false;
130
+ }
131
+ }
125
132
  if (!response.ok) {
126
133
  // Redirect to the current page (window.location) to show the error
127
134
  // This means the page that produced the error is called twice.
@@ -1,8 +1,31 @@
1
- import { describe, expect, it } from "vitest";
2
- import { validateClickEvent } from "./navigation";
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+ import { validateClickEvent, initClientNavigation } from "./navigation";
3
+ // Mocking browser globals
4
+ vi.stubGlobal("window", {
5
+ location: { href: "http://localhost/" },
6
+ addEventListener: vi.fn(),
7
+ history: { scrollRestoration: "auto" },
8
+ });
9
+ vi.stubGlobal("document", {
10
+ addEventListener: vi.fn(),
11
+ });
12
+ vi.stubGlobal("history", {
13
+ scrollRestoration: "auto",
14
+ });
15
+ vi.stubGlobal("Headers", class {
16
+ constructor(init) {
17
+ this.map = {};
18
+ if (init && init.Location) {
19
+ this.map.location = init.Location;
20
+ }
21
+ }
22
+ get(name) {
23
+ return this.map[name.toLowerCase()] || null;
24
+ }
25
+ });
3
26
  describe("clientNavigation", () => {
4
27
  let mockEvent = {
5
- button: 0, // right click
28
+ button: 0,
6
29
  metaKey: false,
7
30
  altKey: false,
8
31
  shiftKey: false,
@@ -52,3 +75,30 @@ describe("clientNavigation", () => {
52
75
  })).toBe(true);
53
76
  });
54
77
  });
78
+ describe("initClientNavigation", () => {
79
+ beforeEach(() => {
80
+ window.location.href = "http://localhost/";
81
+ vi.clearAllMocks();
82
+ });
83
+ it("handleResponse should follow redirects", () => {
84
+ const { handleResponse } = initClientNavigation();
85
+ const mockResponse = {
86
+ status: 302,
87
+ headers: new Headers({ Location: "/new-page" }),
88
+ ok: false,
89
+ };
90
+ const result = handleResponse(mockResponse);
91
+ expect(result).toBe(false);
92
+ expect(window.location.href).toBe("/new-page");
93
+ });
94
+ it("handleResponse should reload on error", () => {
95
+ const { handleResponse } = initClientNavigation();
96
+ const mockResponse = {
97
+ status: 500,
98
+ ok: false,
99
+ };
100
+ const result = handleResponse(mockResponse);
101
+ expect(result).toBe(false);
102
+ expect(window.location.href).toBe("http://localhost/");
103
+ });
104
+ });
@@ -1,8 +1,8 @@
1
- type Interruptor<TArgs extends any[] = any[], TResult = any> = (context: {
1
+ type Interruptor<TArgs extends any[] = any[]> = (context: {
2
2
  request: Request;
3
3
  ctx: Record<string, any>;
4
4
  args: TArgs;
5
- }) => Promise<Response | void | TResult> | Response | void | TResult;
5
+ }) => Promise<Response | void> | Response | void;
6
6
  type ServerFunction<TArgs extends any[] = any[], TResult = any> = (...args: TArgs) => Promise<TResult>;
7
7
  type ServerFunctionOptions = {
8
8
  method?: "GET" | "POST";
@@ -29,7 +29,7 @@ type WrappedServerFunction<TArgs extends any[] = any[], TResult = any> = {
29
29
  * })
30
30
  * ```
31
31
  */
32
- export declare function serverQuery<TArgs extends any[] = any[], TResult = any>(fnsOrFn: ServerFunction<TArgs, TResult> | [...Interruptor<TArgs, TResult>[], ServerFunction<TArgs, TResult>], options?: ServerFunctionOptions): WrappedServerFunction<TArgs, TResult>;
32
+ export declare function serverQuery<TArgs extends any[], TResult>(fnsOrFn: ServerFunction<TArgs, TResult> | [...Interruptor<TArgs>[], ServerFunction<TArgs, TResult>], options?: ServerFunctionOptions): WrappedServerFunction<TArgs, TResult>;
33
33
  /**
34
34
  * Wrap a function to be used as a server action.
35
35
  *
@@ -48,5 +48,5 @@ export declare function serverQuery<TArgs extends any[] = any[], TResult = any>(
48
48
  * })
49
49
  * ```
50
50
  */
51
- export declare function serverAction<TArgs extends any[] = any[], TResult = any>(fnsOrFn: ServerFunction<TArgs, TResult> | [...Interruptor<TArgs, TResult>[], ServerFunction<TArgs, TResult>], options?: ServerFunctionOptions): WrappedServerFunction<TArgs, TResult>;
51
+ export declare function serverAction<TArgs extends any[], TResult>(fnsOrFn: ServerFunction<TArgs, TResult> | [...Interruptor<TArgs>[], ServerFunction<TArgs, TResult>], options?: ServerFunctionOptions): WrappedServerFunction<TArgs, TResult>;
52
52
  export {};
@@ -6,18 +6,14 @@ function createServerFunction(fns, mainFn, options) {
6
6
  for (const fn of fns) {
7
7
  const result = await fn({ request, ctx, args });
8
8
  if (result instanceof Response) {
9
- // We can't easily return a Response from a server action function
10
- // because the return type is expected to be TResult.
11
- // However, if the interruptor returns a Response, it usually means "stop and return this HTTP response".
12
- // In the RSC context, throwing a Response is a common pattern to short-circuit.
13
- throw result;
9
+ // We return the Response so it can be handled by the action handler
10
+ // and serialized into the RSC stream via normalizeActionResult.
11
+ return result;
14
12
  }
15
13
  }
16
14
  return mainFn(...args);
17
15
  };
18
- wrapped.method = options?.method ?? "POST"; // Default to POST if not specified, though user said serverQuery defaults to GET?
19
- // User said: "export const getProject = serverQuery(...) // Defaults to GET"
20
- // So serverQuery defaults to GET, serverAction defaults to POST?
16
+ wrapped.method = options?.method ?? "POST";
21
17
  return wrapped;
22
18
  }
23
19
  /**
@@ -119,6 +119,33 @@ export default serverQuery(async (id) => {
119
119
  export default serverAction(async (id) => {
120
120
  return { id, name: "Project X" };
121
121
  });
122
+ `;
123
+ let SERVER_QUERY_ARRAY_CODE = `
124
+ "use server";
125
+ export const getProject = serverQuery([
126
+ auth,
127
+ async (id) => {
128
+ return { id, name: "Project X" };
129
+ }
130
+ ]);
131
+ `;
132
+ let SERVER_ACTION_ARRAY_CODE = `
133
+ "use server";
134
+ export const upvote = serverAction([
135
+ auth,
136
+ async (id) => {
137
+ return { id, count: 1 };
138
+ }
139
+ ]);
140
+ `;
141
+ let SERVER_QUERY_ARRAY_POST_CODE = `
142
+ "use server";
143
+ export const getProject = serverQuery([
144
+ auth,
145
+ async (id) => {
146
+ return { id, name: "Project X" };
147
+ }
148
+ ], { method: "POST" });
122
149
  `;
123
150
  const TEST_CASES = {
124
151
  COMMENT_CODE,
@@ -137,6 +164,9 @@ export default serverAction(async (id) => {
137
164
  SERVER_ACTION_CODE,
138
165
  SERVER_QUERY_DEFAULT_CODE,
139
166
  SERVER_ACTION_DEFAULT_CODE,
167
+ SERVER_QUERY_ARRAY_CODE,
168
+ SERVER_ACTION_ARRAY_CODE,
169
+ SERVER_QUERY_ARRAY_POST_CODE,
140
170
  };
141
171
  describe("TRANSFORMS", () => {
142
172
  for (const [key, CODE] of Object.entries(TEST_CASES)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.0.0-beta.54",
3
+ "version": "1.0.0-beta.55",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {