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
@@ -0,0 +1,456 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createDefaultNavigationCacheStorage, evictOldGenerationCaches, getCachedNavigationResponse, onNavigationCommit, preloadFromLinkTags, preloadNavigationUrl, } from "./navigationCache";
3
+ describe("navigationCache", () => {
4
+ let mockCacheStorage;
5
+ let mockCache;
6
+ let mockFetch;
7
+ let mockSessionStorage;
8
+ // Local type for requestIdleCallback to avoid depending on global declarations
9
+ let mockRequestIdleCallback;
10
+ beforeEach(() => {
11
+ // Reset module state between tests
12
+ vi.resetModules();
13
+ // Mock Cache
14
+ mockCache = {
15
+ put: vi.fn().mockResolvedValue(undefined),
16
+ match: vi.fn().mockResolvedValue(undefined),
17
+ delete: vi.fn().mockResolvedValue(true),
18
+ add: vi.fn(),
19
+ addAll: vi.fn(),
20
+ keys: vi.fn(),
21
+ matchAll: vi.fn(),
22
+ };
23
+ // Mock CacheStorage
24
+ mockCacheStorage = {
25
+ open: vi.fn().mockResolvedValue(mockCache),
26
+ delete: vi.fn().mockResolvedValue(true),
27
+ keys: vi.fn().mockResolvedValue([]),
28
+ has: vi.fn(),
29
+ match: vi.fn(),
30
+ };
31
+ // Mock fetch
32
+ mockFetch = vi.fn().mockResolvedValue(new Response("test response", {
33
+ status: 200,
34
+ headers: { "content-type": "text/html" },
35
+ }));
36
+ // Mock sessionStorage
37
+ mockSessionStorage = {
38
+ getItem: vi.fn().mockReturnValue(null),
39
+ setItem: vi.fn(),
40
+ removeItem: vi.fn(),
41
+ clear: vi.fn(),
42
+ key: vi.fn(),
43
+ length: 0,
44
+ };
45
+ // Mock requestIdleCallback to execute callback asynchronously for testing
46
+ let idleCallback = null;
47
+ mockRequestIdleCallback = vi.fn((callback) => {
48
+ idleCallback = callback;
49
+ // Execute after current I/O callbacks
50
+ setImmediate(() => {
51
+ if (idleCallback) {
52
+ idleCallback();
53
+ idleCallback = null;
54
+ }
55
+ });
56
+ return 1;
57
+ });
58
+ // Setup global mocks
59
+ globalThis.window = {
60
+ isSecureContext: true,
61
+ location: { origin: "https://example.com" },
62
+ caches: mockCacheStorage,
63
+ fetch: mockFetch,
64
+ sessionStorage: mockSessionStorage,
65
+ crypto: {
66
+ randomUUID: () => "test-uuid-123",
67
+ },
68
+ };
69
+ globalThis.requestIdleCallback = mockRequestIdleCallback;
70
+ });
71
+ describe("createDefaultNavigationCacheStorage", () => {
72
+ it("should create a cache storage wrapper", async () => {
73
+ const env = {
74
+ isSecureContext: true,
75
+ origin: "https://example.com",
76
+ caches: mockCacheStorage,
77
+ fetch: mockFetch,
78
+ };
79
+ const storage = createDefaultNavigationCacheStorage(env);
80
+ expect(storage).toBeDefined();
81
+ const cache = await storage.open("test-cache");
82
+ expect(mockCacheStorage.open).toHaveBeenCalledWith("test-cache");
83
+ expect(cache).toBeDefined();
84
+ });
85
+ it("should return undefined if no caches available", () => {
86
+ const env = {
87
+ isSecureContext: true,
88
+ origin: "https://example.com",
89
+ caches: undefined,
90
+ fetch: mockFetch,
91
+ };
92
+ const storage = createDefaultNavigationCacheStorage(env);
93
+ expect(storage).toBeUndefined();
94
+ });
95
+ });
96
+ describe("preloadNavigationUrl", () => {
97
+ it("should cache a successful response", async () => {
98
+ const env = {
99
+ isSecureContext: true,
100
+ origin: "https://example.com",
101
+ caches: mockCacheStorage,
102
+ fetch: mockFetch,
103
+ };
104
+ const url = new URL("https://example.com/test");
105
+ await preloadNavigationUrl(url, env);
106
+ expect(mockFetch).toHaveBeenCalled();
107
+ expect(mockCacheStorage.open).toHaveBeenCalled();
108
+ expect(mockCache.put).toHaveBeenCalled();
109
+ });
110
+ it("should not cache error responses (status >= 400)", async () => {
111
+ const errorFetch = vi
112
+ .fn()
113
+ .mockResolvedValue(new Response("error", { status: 404 }));
114
+ const env = {
115
+ isSecureContext: true,
116
+ origin: "https://example.com",
117
+ caches: mockCacheStorage,
118
+ fetch: errorFetch,
119
+ };
120
+ const url = new URL("https://example.com/test");
121
+ await preloadNavigationUrl(url, env);
122
+ expect(errorFetch).toHaveBeenCalled();
123
+ expect(mockCache.put).not.toHaveBeenCalled();
124
+ });
125
+ it("should skip cross-origin URLs", async () => {
126
+ const env = {
127
+ isSecureContext: true,
128
+ origin: "https://example.com",
129
+ caches: mockCacheStorage,
130
+ fetch: mockFetch,
131
+ };
132
+ const url = new URL("https://other-origin.com/test");
133
+ await preloadNavigationUrl(url, env);
134
+ expect(mockFetch).not.toHaveBeenCalled();
135
+ expect(mockCache.put).not.toHaveBeenCalled();
136
+ });
137
+ it("should add __rsc query parameter", async () => {
138
+ const env = {
139
+ isSecureContext: true,
140
+ origin: "https://example.com",
141
+ caches: mockCacheStorage,
142
+ fetch: mockFetch,
143
+ };
144
+ const url = new URL("https://example.com/test");
145
+ await preloadNavigationUrl(url, env);
146
+ const fetchCall = mockFetch.mock.calls[0];
147
+ const request = fetchCall[0];
148
+ const requestUrl = new URL(request.url);
149
+ expect(requestUrl.searchParams.has("__rsc")).toBe(true);
150
+ });
151
+ it("should use custom cacheStorage when provided", async () => {
152
+ const customCache = {
153
+ put: vi.fn().mockResolvedValue(undefined),
154
+ match: vi.fn().mockResolvedValue(undefined),
155
+ };
156
+ const customStorage = {
157
+ open: vi.fn().mockResolvedValue(customCache),
158
+ delete: vi.fn().mockResolvedValue(true),
159
+ keys: vi.fn().mockResolvedValue([]),
160
+ };
161
+ const env = {
162
+ isSecureContext: true,
163
+ origin: "https://example.com",
164
+ caches: mockCacheStorage,
165
+ fetch: mockFetch,
166
+ };
167
+ const url = new URL("https://example.com/test");
168
+ await preloadNavigationUrl(url, env, customStorage);
169
+ expect(customStorage.open).toHaveBeenCalled();
170
+ expect(customCache.put).toHaveBeenCalled();
171
+ expect(mockCacheStorage.open).not.toHaveBeenCalled();
172
+ });
173
+ it("should handle errors gracefully", async () => {
174
+ const errorFetch = vi.fn().mockRejectedValue(new Error("Network error"));
175
+ const env = {
176
+ isSecureContext: true,
177
+ origin: "https://example.com",
178
+ caches: mockCacheStorage,
179
+ fetch: errorFetch,
180
+ };
181
+ const url = new URL("https://example.com/test");
182
+ // Should not throw
183
+ await expect(preloadNavigationUrl(url, env)).resolves.toBeUndefined();
184
+ });
185
+ });
186
+ describe("getCachedNavigationResponse", () => {
187
+ it("should return cached response if found", async () => {
188
+ const cachedResponse = new Response("cached", { status: 200 });
189
+ mockCache.match.mockResolvedValue(cachedResponse);
190
+ const env = {
191
+ isSecureContext: true,
192
+ origin: "https://example.com",
193
+ caches: mockCacheStorage,
194
+ fetch: mockFetch,
195
+ };
196
+ const url = new URL("https://example.com/test");
197
+ const result = await getCachedNavigationResponse(url, env);
198
+ expect(result).toBe(cachedResponse);
199
+ expect(mockCache.match).toHaveBeenCalled();
200
+ });
201
+ it("should return undefined if not cached", async () => {
202
+ mockCache.match.mockResolvedValue(undefined);
203
+ const env = {
204
+ isSecureContext: true,
205
+ origin: "https://example.com",
206
+ caches: mockCacheStorage,
207
+ fetch: mockFetch,
208
+ };
209
+ const url = new URL("https://example.com/test");
210
+ const result = await getCachedNavigationResponse(url, env);
211
+ expect(result).toBeUndefined();
212
+ });
213
+ it("should check global cacheStorage if not provided", async () => {
214
+ const cachedResponse = new Response("cached", { status: 200 });
215
+ const customCache = {
216
+ put: vi.fn(),
217
+ match: vi.fn().mockResolvedValue(cachedResponse),
218
+ };
219
+ const customStorage = {
220
+ open: vi.fn().mockResolvedValue(customCache),
221
+ delete: vi.fn(),
222
+ keys: vi.fn().mockResolvedValue([]),
223
+ };
224
+ // Set global cache storage
225
+ globalThis.__rsc_cacheStorage = customStorage;
226
+ const url = new URL("https://example.com/test");
227
+ const result = await getCachedNavigationResponse(url);
228
+ expect(result).toBe(cachedResponse);
229
+ expect(customStorage.open).toHaveBeenCalled();
230
+ // Cleanup
231
+ delete globalThis.__rsc_cacheStorage;
232
+ });
233
+ it("should add __rsc query parameter", async () => {
234
+ const env = {
235
+ isSecureContext: true,
236
+ origin: "https://example.com",
237
+ caches: mockCacheStorage,
238
+ fetch: mockFetch,
239
+ };
240
+ const url = new URL("https://example.com/test");
241
+ await getCachedNavigationResponse(url, env);
242
+ const matchCall = mockCache.match.mock.calls[0];
243
+ const request = matchCall[0];
244
+ const requestUrl = new URL(request.url);
245
+ expect(requestUrl.searchParams.has("__rsc")).toBe(true);
246
+ });
247
+ it("should skip cross-origin URLs", async () => {
248
+ const env = {
249
+ isSecureContext: true,
250
+ origin: "https://example.com",
251
+ caches: mockCacheStorage,
252
+ fetch: mockFetch,
253
+ };
254
+ const url = new URL("https://other-origin.com/test");
255
+ const result = await getCachedNavigationResponse(url, env);
256
+ expect(result).toBeUndefined();
257
+ expect(mockCache.match).not.toHaveBeenCalled();
258
+ });
259
+ it("should handle errors gracefully", async () => {
260
+ mockCache.match.mockRejectedValue(new Error("Cache error"));
261
+ const env = {
262
+ isSecureContext: true,
263
+ origin: "https://example.com",
264
+ caches: mockCacheStorage,
265
+ fetch: mockFetch,
266
+ };
267
+ const url = new URL("https://example.com/test");
268
+ // Should not throw
269
+ const result = await getCachedNavigationResponse(url, env);
270
+ expect(result).toBeUndefined();
271
+ });
272
+ });
273
+ describe("evictOldGenerationCaches", () => {
274
+ it("should delete old generation caches", async () => {
275
+ const env = {
276
+ isSecureContext: true,
277
+ origin: "https://example.com",
278
+ caches: mockCacheStorage,
279
+ fetch: mockFetch,
280
+ };
281
+ // Get the actual tabId that will be used
282
+ const url = new URL("https://example.com/test");
283
+ await preloadNavigationUrl(url, env);
284
+ const openCall = mockCacheStorage.open.mock.calls[0];
285
+ const cacheName = openCall[0];
286
+ const tabIdMatch = cacheName.match(/^rsc-prefetch:rwsdk:([^:]+):\d+$/);
287
+ const tabId = tabIdMatch ? tabIdMatch[1] : "test-uuid-123";
288
+ // Increment generation to 2 by calling onNavigationCommit twice
289
+ onNavigationCommit(env);
290
+ onNavigationCommit(env);
291
+ // Mock cache names matching the actual tabId
292
+ const allCacheNames = [
293
+ `rsc-prefetch:rwsdk:${tabId}:0`,
294
+ `rsc-prefetch:rwsdk:${tabId}:1`,
295
+ `rsc-prefetch:rwsdk:${tabId}:2`,
296
+ "rsc-prefetch:rwsdk:other-tab:0",
297
+ ];
298
+ mockCacheStorage.keys.mockResolvedValue(allCacheNames);
299
+ await evictOldGenerationCaches(env);
300
+ // Wait for the cleanup to execute
301
+ await new Promise((resolve) => setTimeout(resolve, 10));
302
+ // Should delete generations 0 and 1, but not 2 (current) or other-tab
303
+ expect(mockCacheStorage.delete).toHaveBeenCalledWith(`rsc-prefetch:rwsdk:${tabId}:0`);
304
+ expect(mockCacheStorage.delete).toHaveBeenCalledWith(`rsc-prefetch:rwsdk:${tabId}:1`);
305
+ expect(mockCacheStorage.delete).not.toHaveBeenCalledWith(`rsc-prefetch:rwsdk:${tabId}:2`);
306
+ expect(mockCacheStorage.delete).not.toHaveBeenCalledWith("rsc-prefetch:rwsdk:other-tab:0");
307
+ });
308
+ it("should use custom cacheStorage when provided", async () => {
309
+ // Get the actual tabId that will be used
310
+ const env = {
311
+ isSecureContext: true,
312
+ origin: "https://example.com",
313
+ caches: mockCacheStorage,
314
+ fetch: mockFetch,
315
+ };
316
+ const url = new URL("https://example.com/test");
317
+ await preloadNavigationUrl(url, env);
318
+ const openCall = mockCacheStorage.open.mock.calls[0];
319
+ const cacheName = openCall[0];
320
+ const tabIdMatch = cacheName.match(/^rsc-prefetch:rwsdk:([^:]+):\d+$/);
321
+ const tabId = tabIdMatch ? tabIdMatch[1] : "test-uuid-123";
322
+ const customStorage = {
323
+ open: vi.fn(),
324
+ delete: vi.fn().mockResolvedValue(true),
325
+ keys: vi
326
+ .fn()
327
+ .mockResolvedValue([
328
+ `rsc-prefetch:rwsdk:${tabId}:0`,
329
+ `rsc-prefetch:rwsdk:${tabId}:1`,
330
+ ]),
331
+ };
332
+ // Increment generation so there are old caches to delete
333
+ onNavigationCommit();
334
+ await evictOldGenerationCaches(undefined, customStorage);
335
+ // Wait for the cleanup to execute
336
+ await new Promise((resolve) => setTimeout(resolve, 10));
337
+ expect(customStorage.keys).toHaveBeenCalled();
338
+ expect(customStorage.delete).toHaveBeenCalled();
339
+ });
340
+ it("should handle errors gracefully", async () => {
341
+ mockCacheStorage.keys.mockRejectedValue(new Error("Cache error"));
342
+ const env = {
343
+ isSecureContext: true,
344
+ origin: "https://example.com",
345
+ caches: mockCacheStorage,
346
+ fetch: mockFetch,
347
+ };
348
+ // Should not throw
349
+ await expect(evictOldGenerationCaches(env)).resolves.toBeUndefined();
350
+ // Wait for requestIdleCallback to execute
351
+ await new Promise((resolve) => setTimeout(resolve, 10));
352
+ });
353
+ });
354
+ describe("onNavigationCommit", () => {
355
+ it("should increment generation and evict old caches", async () => {
356
+ const env = {
357
+ isSecureContext: true,
358
+ origin: "https://example.com",
359
+ caches: mockCacheStorage,
360
+ fetch: mockFetch,
361
+ };
362
+ mockCacheStorage.keys.mockResolvedValue([]);
363
+ onNavigationCommit(env);
364
+ // Wait for eviction to complete
365
+ await new Promise((resolve) => setTimeout(resolve, 10));
366
+ expect(mockRequestIdleCallback).toHaveBeenCalled();
367
+ });
368
+ });
369
+ describe("preloadFromLinkTags", () => {
370
+ it("should preload URLs from prefetch link tags", async () => {
371
+ const env = {
372
+ isSecureContext: true,
373
+ origin: "https://example.com",
374
+ caches: mockCacheStorage,
375
+ fetch: mockFetch,
376
+ };
377
+ // Create a mock document with prefetch links
378
+ const mockDoc = {
379
+ querySelectorAll: vi.fn().mockReturnValue([
380
+ {
381
+ getAttribute: () => "/page1",
382
+ },
383
+ {
384
+ getAttribute: () => "/page2",
385
+ },
386
+ ]),
387
+ };
388
+ await preloadFromLinkTags(mockDoc, env);
389
+ // Should have called preloadNavigationUrl for each link
390
+ expect(mockFetch).toHaveBeenCalled();
391
+ });
392
+ it("should skip non-route-like hrefs (not starting with /)", async () => {
393
+ const env = {
394
+ isSecureContext: true,
395
+ origin: "https://example.com",
396
+ caches: mockCacheStorage,
397
+ fetch: mockFetch,
398
+ };
399
+ const mockDoc = {
400
+ querySelectorAll: vi.fn().mockReturnValue([
401
+ {
402
+ getAttribute: () => "https://example.com/page1",
403
+ },
404
+ {
405
+ getAttribute: () => "/page2",
406
+ },
407
+ ]),
408
+ };
409
+ await preloadFromLinkTags(mockDoc, env);
410
+ // Should only preload /page2, not the absolute URL
411
+ expect(mockFetch).toHaveBeenCalledTimes(1);
412
+ });
413
+ it("should use custom cacheStorage when provided", async () => {
414
+ const customCache = {
415
+ put: vi.fn().mockResolvedValue(undefined),
416
+ match: vi.fn(),
417
+ };
418
+ const customStorage = {
419
+ open: vi.fn().mockResolvedValue(customCache),
420
+ delete: vi.fn(),
421
+ keys: vi.fn().mockResolvedValue([]),
422
+ };
423
+ const env = {
424
+ isSecureContext: true,
425
+ origin: "https://example.com",
426
+ caches: mockCacheStorage,
427
+ fetch: mockFetch,
428
+ };
429
+ const mockDoc = {
430
+ querySelectorAll: vi.fn().mockReturnValue([
431
+ {
432
+ getAttribute: () => "/page1",
433
+ },
434
+ ]),
435
+ };
436
+ await preloadFromLinkTags(mockDoc, env, customStorage);
437
+ expect(customStorage.open).toHaveBeenCalled();
438
+ expect(customCache.put).toHaveBeenCalled();
439
+ });
440
+ });
441
+ describe("cache name generation", () => {
442
+ it("should generate cache names with correct format", async () => {
443
+ const env = {
444
+ isSecureContext: true,
445
+ origin: "https://example.com",
446
+ caches: mockCacheStorage,
447
+ fetch: mockFetch,
448
+ };
449
+ const url = new URL("https://example.com/test");
450
+ await preloadNavigationUrl(url, env);
451
+ const openCall = mockCacheStorage.open.mock.calls[0];
452
+ const cacheName = openCall[0];
453
+ expect(cacheName).toMatch(/^rsc-prefetch:rwsdk:[^:]+:\d+$/);
454
+ });
455
+ });
456
+ });
@@ -1,13 +1,35 @@
1
1
  import type { CallServerCallback } from "react-server-dom-webpack/client.browser";
2
2
  export type { HydrationOptions } from "react-dom/client";
3
3
  export type { CallServerCallback } from "react-server-dom-webpack/client.browser";
4
- export type ActionResponse<Result> = {
4
+ export type RscActionResponse<Result> = {
5
5
  node: React.ReactNode;
6
6
  actionResult: Result;
7
7
  };
8
+ export type ActionResponseData = {
9
+ status: number;
10
+ headers: {
11
+ location: string | null;
12
+ };
13
+ };
14
+ export type ActionResponseMeta = {
15
+ __rw_action_response: ActionResponseData;
16
+ };
17
+ export declare function isActionResponse(value: unknown): value is ActionResponseMeta;
8
18
  export type TransportContext = {
9
- setRscPayload: <Result>(v: Promise<ActionResponse<Result>>) => void;
19
+ setRscPayload: <Result>(v: Promise<RscActionResponse<Result>>) => void;
10
20
  handleResponse?: (response: Response) => boolean;
21
+ /**
22
+ * Optional callback invoked after a new RSC payload has been committed on the client.
23
+ * This is useful for features like client-side navigation that want to run logic
24
+ * after hydration/updates, e.g. warming navigation caches.
25
+ */
26
+ onHydrationUpdate?: () => void;
27
+ /**
28
+ * Optional callback invoked when an action returns a Response.
29
+ * Return true to signal that the response has been handled and
30
+ * default behaviour (e.g. redirects) should be skipped.
31
+ */
32
+ onActionResponse?: (actionResponse: ActionResponseData) => boolean | void;
11
33
  };
12
34
  export type Transport = (context: TransportContext) => CallServerCallback;
13
- export type CreateCallServer = (context: TransportContext) => <Result>(id: null | string, args: null | unknown[]) => Promise<Result>;
35
+ export type CreateCallServer = (context: TransportContext) => <Result>(id: null | string, args: null | unknown[], source?: "action" | "navigation") => Promise<Result>;
@@ -1 +1,7 @@
1
- export {};
1
+ export function isActionResponse(value) {
2
+ return (typeof value === "object" &&
3
+ value !== null &&
4
+ "__rw_action_response" in value &&
5
+ typeof value.__rw_action_response === "object" &&
6
+ value.__rw_action_response !== null);
7
+ }
@@ -3,6 +3,8 @@
3
3
  // prettier-ignore
4
4
  import { initClient } from "../../client/client";
5
5
  // prettier-ignore
6
+ import { isActionResponse, } from "../../client/types";
7
+ // prettier-ignore
6
8
  import { createFromReadableStream } from "react-server-dom-webpack/client.browser";
7
9
  // prettier-ignore
8
10
  import { MESSAGE_TYPE } from "./shared";
@@ -103,7 +105,21 @@ export const realtimeTransport = ({ key = DEFAULT_KEY, handleResponse, }) => (tr
103
105
  callServer: realtimeCallServer,
104
106
  });
105
107
  transportContext.setRscPayload(rscPayload);
106
- return (await rscPayload).actionResult;
108
+ const rawActionResult = (await rscPayload).actionResult;
109
+ if (isActionResponse(rawActionResult)) {
110
+ const actionResponse = rawActionResult.__rw_action_response;
111
+ const handledByHook = transportContext.onActionResponse?.(actionResponse) === true;
112
+ if (!handledByHook) {
113
+ const location = actionResponse.headers["location"];
114
+ const isRedirect = actionResponse.status >= 300 && actionResponse.status < 400;
115
+ if (location && isRedirect) {
116
+ window.location.href = location;
117
+ return null;
118
+ }
119
+ }
120
+ return rawActionResult;
121
+ }
122
+ return rawActionResult;
107
123
  }
108
124
  catch (err) {
109
125
  throw err;
@@ -34,7 +34,14 @@ function rechunkStream(stream, maxChunkSize = 28) {
34
34
  }
35
35
  export const normalizeActionResult = (actionResult) => {
36
36
  if (actionResult instanceof Response) {
37
- return null;
37
+ return {
38
+ __rw_action_response: {
39
+ status: actionResult.status,
40
+ headers: {
41
+ location: actionResult.headers.get("location"),
42
+ },
43
+ },
44
+ };
38
45
  }
39
46
  if (actionResult instanceof ReadableStream) {
40
47
  return rechunkStream(actionResult);
@@ -1,17 +1,32 @@
1
1
  import { RpcStub } from "capnweb";
2
2
  import { DurableObject } from "cloudflare:workers";
3
+ import type { RequestInfo } from "../runtime/requestInfo/types";
3
4
  export type SyncedStateValue = unknown;
4
- type OnSetHandler = (key: string, value: SyncedStateValue) => void;
5
- type OnGetHandler = (key: string, value: SyncedStateValue | undefined) => void;
5
+ type OnSetHandler = (key: string, value: SyncedStateValue, stub: DurableObjectStub<SyncedStateServer>) => void;
6
+ type OnGetHandler = (key: string, value: SyncedStateValue | undefined, stub: DurableObjectStub<SyncedStateServer>) => void;
7
+ type OnKeyHandler = (key: string, stub: DurableObjectStub<SyncedStateServer>) => Promise<string>;
8
+ type OnRoomHandler = (roomId: string | undefined, requestInfo: RequestInfo | null) => Promise<string>;
9
+ type OnSubscribeHandler = (key: string, stub: DurableObjectStub<SyncedStateServer>) => void;
10
+ type OnUnsubscribeHandler = (key: string, stub: DurableObjectStub<SyncedStateServer>) => void;
6
11
  /**
7
12
  * Durable Object that keeps shared state for multiple clients and notifies subscribers.
8
13
  */
9
14
  export declare class SyncedStateServer extends DurableObject {
10
15
  #private;
11
- static registerKeyHandler(handler: (key: string) => Promise<string>): void;
12
- static getKeyHandler(): ((key: string) => Promise<string>) | null;
16
+ static registerKeyHandler(handler: OnKeyHandler | null): void;
17
+ static getKeyHandler(): OnKeyHandler | null;
18
+ static registerRoomHandler(handler: OnRoomHandler | null): void;
19
+ static getRoomHandler(): OnRoomHandler | null;
20
+ static registerNamespace(namespace: DurableObjectNamespace<SyncedStateServer>, durableObjectName?: string): void;
21
+ static getNamespace(): DurableObjectNamespace<SyncedStateServer> | null;
22
+ static getDurableObjectName(): string;
23
+ setStub(stub: DurableObjectStub<SyncedStateServer>): void;
13
24
  static registerSetStateHandler(handler: OnSetHandler | null): void;
14
25
  static registerGetStateHandler(handler: OnGetHandler | null): void;
26
+ static registerSubscribeHandler(handler: OnSubscribeHandler | null): void;
27
+ static registerUnsubscribeHandler(handler: OnUnsubscribeHandler | null): void;
28
+ static getSubscribeHandler(): OnSubscribeHandler | null;
29
+ static getUnsubscribeHandler(): OnUnsubscribeHandler | null;
15
30
  getState(key: string): SyncedStateValue;
16
31
  setState(value: SyncedStateValue, key: string): void;
17
32
  subscribe(key: string, client: RpcStub<(value: SyncedStateValue) => void>): void;