rwsdk 1.0.0-beta.42 → 1.0.0-beta.44
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/runtime/client/client.d.ts +22 -1
- package/dist/runtime/client/client.js +23 -2
- package/dist/runtime/entries/no-react-server-ssr-bridge.js +1 -1
- package/dist/runtime/entries/no-react-server.js +3 -1
- package/dist/runtime/entries/react-server-only.js +1 -1
- package/dist/runtime/entries/worker.d.ts +1 -0
- package/dist/runtime/entries/worker.js +1 -0
- package/dist/runtime/lib/links.js +6 -6
- package/dist/runtime/lib/router.d.ts +19 -17
- package/dist/runtime/lib/router.js +343 -108
- package/dist/runtime/lib/router.test.js +343 -1
- package/dist/runtime/requestInfo/types.d.ts +1 -0
- package/dist/runtime/requestInfo/utils.js +1 -0
- package/dist/runtime/requestInfo/worker.js +2 -1
- package/dist/runtime/worker.js +6 -1
- package/dist/use-synced-state/SyncedStateServer.d.mts +19 -4
- package/dist/use-synced-state/SyncedStateServer.mjs +76 -8
- package/dist/use-synced-state/__tests__/SyncStateServer.test.mjs +18 -11
- package/dist/use-synced-state/__tests__/worker.test.mjs +13 -12
- package/dist/use-synced-state/client-core.d.ts +3 -0
- package/dist/use-synced-state/client-core.js +77 -13
- package/dist/use-synced-state/useSyncedState.d.ts +3 -2
- package/dist/use-synced-state/useSyncedState.js +9 -3
- package/dist/use-synced-state/worker.d.mts +2 -1
- package/dist/use-synced-state/worker.mjs +82 -16
- package/dist/vite/runDirectivesScan.mjs +3 -1
- package/dist/vite/transformClientComponents.test.mjs +32 -0
- package/package.json +7 -3
|
@@ -46,9 +46,12 @@ describe("SyncedStateServer", () => {
|
|
|
46
46
|
});
|
|
47
47
|
it("invokes registered onSet handler", () => {
|
|
48
48
|
const coordinator = new SyncedStateServer({}, {});
|
|
49
|
+
const mockStub = {};
|
|
50
|
+
coordinator.setStub(mockStub);
|
|
49
51
|
const calls = [];
|
|
50
|
-
SyncedStateServer.registerSetStateHandler((key, value) => {
|
|
52
|
+
SyncedStateServer.registerSetStateHandler((key, value, stub) => {
|
|
51
53
|
calls.push({ key, value });
|
|
54
|
+
expect(stub).toBe(mockStub);
|
|
52
55
|
});
|
|
53
56
|
coordinator.setState(2, "counter");
|
|
54
57
|
expect(calls).toEqual([{ key: "counter", value: 2 }]);
|
|
@@ -56,9 +59,12 @@ describe("SyncedStateServer", () => {
|
|
|
56
59
|
});
|
|
57
60
|
it("invokes registered onGet handler", () => {
|
|
58
61
|
const coordinator = new SyncedStateServer({}, {});
|
|
62
|
+
const mockStub = {};
|
|
63
|
+
coordinator.setStub(mockStub);
|
|
59
64
|
const calls = [];
|
|
60
|
-
SyncedStateServer.registerGetStateHandler((key, value) => {
|
|
65
|
+
SyncedStateServer.registerGetStateHandler((key, value, stub) => {
|
|
61
66
|
calls.push({ key, value });
|
|
67
|
+
expect(stub).toBe(mockStub);
|
|
62
68
|
});
|
|
63
69
|
coordinator.setState(4, "counter");
|
|
64
70
|
expect(coordinator.getState("counter")).toBe(4);
|
|
@@ -66,43 +72,44 @@ describe("SyncedStateServer", () => {
|
|
|
66
72
|
SyncedStateServer.registerGetStateHandler(null);
|
|
67
73
|
});
|
|
68
74
|
describe("registerKeyHandler", () => {
|
|
75
|
+
const mockStub = {};
|
|
69
76
|
afterEach(() => {
|
|
70
|
-
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
77
|
+
SyncedStateServer.registerKeyHandler(async (key, stub) => key);
|
|
71
78
|
});
|
|
72
79
|
it("stores and retrieves the registered handler", async () => {
|
|
73
|
-
const handler = async (key) => `transformed:${key}`;
|
|
80
|
+
const handler = async (key, stub) => `transformed:${key}`;
|
|
74
81
|
SyncedStateServer.registerKeyHandler(handler);
|
|
75
82
|
const retrievedHandler = SyncedStateServer.getKeyHandler();
|
|
76
83
|
expect(retrievedHandler).toBe(handler);
|
|
77
84
|
});
|
|
78
85
|
it("transforms keys using the registered handler", async () => {
|
|
79
|
-
const handler = async (key) => `user:123:${key}`;
|
|
86
|
+
const handler = async (key, stub) => `user:123:${key}`;
|
|
80
87
|
SyncedStateServer.registerKeyHandler(handler);
|
|
81
|
-
const result = await handler("counter");
|
|
88
|
+
const result = await handler("counter", mockStub);
|
|
82
89
|
expect(result).toBe("user:123:counter");
|
|
83
90
|
});
|
|
84
91
|
it("returns null when no handler is registered", () => {
|
|
85
|
-
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
92
|
+
SyncedStateServer.registerKeyHandler(async (key, stub) => key);
|
|
86
93
|
const handler = SyncedStateServer.getKeyHandler();
|
|
87
94
|
expect(handler).not.toBeNull();
|
|
88
95
|
});
|
|
89
96
|
it("allows handler to be async", async () => {
|
|
90
|
-
const handler = async (key) => {
|
|
97
|
+
const handler = async (key, stub) => {
|
|
91
98
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
92
99
|
return `async:${key}`;
|
|
93
100
|
};
|
|
94
101
|
SyncedStateServer.registerKeyHandler(handler);
|
|
95
|
-
const result = await handler("test");
|
|
102
|
+
const result = await handler("test", mockStub);
|
|
96
103
|
expect(result).toBe("async:test");
|
|
97
104
|
});
|
|
98
105
|
it("handler receives the correct key parameter", async () => {
|
|
99
106
|
let receivedKey = "";
|
|
100
|
-
const handler = async (key) => {
|
|
107
|
+
const handler = async (key, stub) => {
|
|
101
108
|
receivedKey = key;
|
|
102
109
|
return key;
|
|
103
110
|
};
|
|
104
111
|
SyncedStateServer.registerKeyHandler(handler);
|
|
105
|
-
await handler("myKey");
|
|
112
|
+
await handler("myKey", mockStub);
|
|
106
113
|
expect(receivedKey).toBe("myKey");
|
|
107
114
|
});
|
|
108
115
|
});
|
|
@@ -18,52 +18,53 @@ describe("SyncedStateProxy", () => {
|
|
|
18
18
|
beforeEach(() => {
|
|
19
19
|
mockCoordinator = new SyncedStateServer({}, {});
|
|
20
20
|
});
|
|
21
|
+
const mockStub = {};
|
|
21
22
|
afterEach(() => {
|
|
22
|
-
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
23
|
+
SyncedStateServer.registerKeyHandler(async (key, stub) => key);
|
|
23
24
|
});
|
|
24
25
|
it("transforms keys before calling coordinator methods when handler is registered", async () => {
|
|
25
|
-
const handler = async (key) => `transformed:${key}`;
|
|
26
|
+
const handler = async (key, stub) => `transformed:${key}`;
|
|
26
27
|
SyncedStateServer.registerKeyHandler(handler);
|
|
27
|
-
const transformedKey = await handler("counter");
|
|
28
|
+
const transformedKey = await handler("counter", mockStub);
|
|
28
29
|
expect(transformedKey).toBe("transformed:counter");
|
|
29
30
|
mockCoordinator.setState(5, transformedKey);
|
|
30
31
|
const value = mockCoordinator.getState(transformedKey);
|
|
31
32
|
expect(value).toBe(5);
|
|
32
33
|
});
|
|
33
34
|
it("does not transform keys when no handler is registered", () => {
|
|
34
|
-
SyncedStateServer.registerKeyHandler(async (key) => key);
|
|
35
|
+
SyncedStateServer.registerKeyHandler(async (key, stub) => key);
|
|
35
36
|
const handler = SyncedStateServer.getKeyHandler();
|
|
36
37
|
expect(handler).not.toBeNull();
|
|
37
38
|
});
|
|
38
39
|
it("passes through original key when handler returns it unchanged", async () => {
|
|
39
|
-
const handler = async (key) => key;
|
|
40
|
+
const handler = async (key, stub) => key;
|
|
40
41
|
SyncedStateServer.registerKeyHandler(handler);
|
|
41
|
-
const result = await handler("counter");
|
|
42
|
+
const result = await handler("counter", mockStub);
|
|
42
43
|
expect(result).toBe("counter");
|
|
43
44
|
});
|
|
44
45
|
it("handler can scope keys per user", async () => {
|
|
45
|
-
const handler = async (key) => {
|
|
46
|
+
const handler = async (key, stub) => {
|
|
46
47
|
const userId = "user123";
|
|
47
48
|
return `user:${userId}:${key}`;
|
|
48
49
|
};
|
|
49
50
|
SyncedStateServer.registerKeyHandler(handler);
|
|
50
|
-
const result = await handler("settings");
|
|
51
|
+
const result = await handler("settings", mockStub);
|
|
51
52
|
expect(result).toBe("user:user123:settings");
|
|
52
53
|
});
|
|
53
54
|
it("allows errors from handler to propagate", async () => {
|
|
54
|
-
const handler = async (_key) => {
|
|
55
|
+
const handler = async (_key, stub) => {
|
|
55
56
|
throw new Error("Handler error");
|
|
56
57
|
};
|
|
57
58
|
SyncedStateServer.registerKeyHandler(handler);
|
|
58
|
-
await expect(handler("test")).rejects.toThrow("Handler error");
|
|
59
|
+
await expect(handler("test", mockStub)).rejects.toThrow("Handler error");
|
|
59
60
|
});
|
|
60
61
|
it("handles async operations in handler", async () => {
|
|
61
|
-
const handler = async (key) => {
|
|
62
|
+
const handler = async (key, stub) => {
|
|
62
63
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
63
64
|
return `async:${key}`;
|
|
64
65
|
};
|
|
65
66
|
SyncedStateServer.registerKeyHandler(handler);
|
|
66
|
-
const result = await handler("data");
|
|
67
|
+
const result = await handler("data", mockStub);
|
|
67
68
|
expect(result).toBe("async:data");
|
|
68
69
|
});
|
|
69
70
|
});
|
|
@@ -6,12 +6,14 @@ export type SyncedStateClient = {
|
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
8
|
* Returns a cached client for the provided endpoint, creating it when necessary.
|
|
9
|
+
* The client is wrapped to track subscriptions for cleanup on page reload.
|
|
9
10
|
* @param endpoint Endpoint to connect to.
|
|
10
11
|
* @returns RPC client instance.
|
|
11
12
|
*/
|
|
12
13
|
export declare const getSyncedStateClient: (endpoint?: string) => SyncedStateClient;
|
|
13
14
|
/**
|
|
14
15
|
* Initializes and caches an RPC client instance for the sync state endpoint.
|
|
16
|
+
* The client is wrapped to track subscriptions for cleanup on page reload.
|
|
15
17
|
* @param options Optional endpoint override.
|
|
16
18
|
* @returns Cached client instance or `null` when running without `window`.
|
|
17
19
|
*/
|
|
@@ -20,6 +22,7 @@ export declare const initSyncedStateClient: (options?: {
|
|
|
20
22
|
}) => SyncedStateClient | null;
|
|
21
23
|
/**
|
|
22
24
|
* Injects a client instance for tests and updates the cached endpoint.
|
|
25
|
+
* Also clears the subscription registry for test isolation.
|
|
23
26
|
* @param client Stub client instance or `null` to clear the cache.
|
|
24
27
|
* @param endpoint Endpoint associated with the injected client.
|
|
25
28
|
*/
|
|
@@ -1,39 +1,103 @@
|
|
|
1
1
|
import { newWebSocketRpcSession } from "capnweb";
|
|
2
2
|
import { DEFAULT_SYNCED_STATE_PATH } from "./constants.mjs";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// Map of endpoint URLs to their respective clients
|
|
4
|
+
const clientCache = new Map();
|
|
5
|
+
const activeSubscriptions = new Set();
|
|
6
|
+
// Set up beforeunload handler to unsubscribe all active subscriptions
|
|
7
|
+
if (typeof window !== "undefined") {
|
|
8
|
+
const handleBeforeUnload = () => {
|
|
9
|
+
if (activeSubscriptions.size === 0) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
// Unsubscribe all active subscriptions
|
|
13
|
+
// Use a synchronous approach where possible, but don't block page unload
|
|
14
|
+
const subscriptions = Array.from(activeSubscriptions);
|
|
15
|
+
activeSubscriptions.clear();
|
|
16
|
+
// Fire-and-forget unsubscribe calls - we can't await during beforeunload
|
|
17
|
+
for (const { key, handler, client } of subscriptions) {
|
|
18
|
+
void client.unsubscribe(key, handler).catch(() => {
|
|
19
|
+
// Ignore errors during page unload - the connection will be closed anyway
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
26
|
* Returns a cached client for the provided endpoint, creating it when necessary.
|
|
27
|
+
* The client is wrapped to track subscriptions for cleanup on page reload.
|
|
7
28
|
* @param endpoint Endpoint to connect to.
|
|
8
29
|
* @returns RPC client instance.
|
|
9
30
|
*/
|
|
10
|
-
export const getSyncedStateClient = (endpoint =
|
|
11
|
-
if
|
|
12
|
-
|
|
31
|
+
export const getSyncedStateClient = (endpoint = DEFAULT_SYNCED_STATE_PATH) => {
|
|
32
|
+
// Return existing client if already cached for this endpoint
|
|
33
|
+
const existingClient = clientCache.get(endpoint);
|
|
34
|
+
if (existingClient) {
|
|
35
|
+
return existingClient;
|
|
13
36
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
37
|
+
const baseClient = newWebSocketRpcSession(endpoint);
|
|
38
|
+
// Wrap the client using a Proxy to track subscriptions
|
|
39
|
+
// The RPC client uses dynamic property access, so we can't use .bind()
|
|
40
|
+
const wrappedClient = new Proxy(baseClient, {
|
|
41
|
+
get(target, prop) {
|
|
42
|
+
if (prop === "subscribe") {
|
|
43
|
+
return async (key, handler) => {
|
|
44
|
+
const subscription = {
|
|
45
|
+
key,
|
|
46
|
+
handler,
|
|
47
|
+
client: wrappedClient,
|
|
48
|
+
};
|
|
49
|
+
activeSubscriptions.add(subscription);
|
|
50
|
+
return target[prop](key, handler);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (prop === "unsubscribe") {
|
|
54
|
+
return async (key, handler) => {
|
|
55
|
+
// Find and remove the subscription
|
|
56
|
+
for (const sub of activeSubscriptions) {
|
|
57
|
+
if (sub.key === key &&
|
|
58
|
+
sub.handler === handler &&
|
|
59
|
+
sub.client === wrappedClient) {
|
|
60
|
+
activeSubscriptions.delete(sub);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return target[prop](key, handler);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Pass through all other properties/methods
|
|
68
|
+
return target[prop];
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
// Cache the client for this endpoint
|
|
72
|
+
clientCache.set(endpoint, wrappedClient);
|
|
73
|
+
return wrappedClient;
|
|
17
74
|
};
|
|
18
75
|
/**
|
|
19
76
|
* Initializes and caches an RPC client instance for the sync state endpoint.
|
|
77
|
+
* The client is wrapped to track subscriptions for cleanup on page reload.
|
|
20
78
|
* @param options Optional endpoint override.
|
|
21
79
|
* @returns Cached client instance or `null` when running without `window`.
|
|
22
80
|
*/
|
|
23
81
|
export const initSyncedStateClient = (options = {}) => {
|
|
24
|
-
|
|
82
|
+
const endpoint = options.endpoint ?? DEFAULT_SYNCED_STATE_PATH;
|
|
25
83
|
if (typeof window === "undefined") {
|
|
26
84
|
return null;
|
|
27
85
|
}
|
|
28
|
-
|
|
29
|
-
return
|
|
86
|
+
// Use getSyncedStateClient which now handles caching via Map
|
|
87
|
+
return getSyncedStateClient(endpoint);
|
|
30
88
|
};
|
|
31
89
|
/**
|
|
32
90
|
* Injects a client instance for tests and updates the cached endpoint.
|
|
91
|
+
* Also clears the subscription registry for test isolation.
|
|
33
92
|
* @param client Stub client instance or `null` to clear the cache.
|
|
34
93
|
* @param endpoint Endpoint associated with the injected client.
|
|
35
94
|
*/
|
|
36
95
|
export const setSyncedStateClientForTesting = (client, endpoint = DEFAULT_SYNCED_STATE_PATH) => {
|
|
37
|
-
|
|
38
|
-
|
|
96
|
+
if (client) {
|
|
97
|
+
clientCache.set(endpoint, client);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
clientCache.delete(endpoint);
|
|
101
|
+
}
|
|
102
|
+
activeSubscriptions.clear();
|
|
39
103
|
};
|
|
@@ -8,6 +8,7 @@ type HookDeps = {
|
|
|
8
8
|
type Setter<T> = (value: T | ((previous: T) => T)) => void;
|
|
9
9
|
export type CreateSyncedStateHookOptions = {
|
|
10
10
|
url?: string;
|
|
11
|
+
roomId?: string;
|
|
11
12
|
hooks?: HookDeps;
|
|
12
13
|
};
|
|
13
14
|
/**
|
|
@@ -15,6 +16,6 @@ export type CreateSyncedStateHookOptions = {
|
|
|
15
16
|
* @param options Optional overrides for endpoint and React primitives.
|
|
16
17
|
* @returns Hook that syncs state through the sync state service.
|
|
17
18
|
*/
|
|
18
|
-
export declare const createSyncedStateHook: (options?: CreateSyncedStateHookOptions) => <T>(initialValue: T, key: string) => [T, Setter<T>];
|
|
19
|
-
export declare const useSyncedState: <T>(initialValue: T, key: string) => [T, Setter<T>];
|
|
19
|
+
export declare const createSyncedStateHook: (options?: CreateSyncedStateHookOptions) => <T>(initialValue: T, key: string, roomId?: string | undefined) => [T, Setter<T>];
|
|
20
|
+
export declare const useSyncedState: <T>(initialValue: T, key: string, roomId?: string | undefined) => [T, Setter<T>];
|
|
20
21
|
export {};
|
|
@@ -13,13 +13,14 @@ const defaultDeps = {
|
|
|
13
13
|
* @returns Hook that syncs state through the sync state service.
|
|
14
14
|
*/
|
|
15
15
|
export const createSyncedStateHook = (options = {}) => {
|
|
16
|
-
const
|
|
16
|
+
const basePath = options.url ?? DEFAULT_SYNCED_STATE_PATH;
|
|
17
17
|
const deps = options.hooks ?? defaultDeps;
|
|
18
18
|
const { useState, useEffect, useRef, useCallback } = deps;
|
|
19
|
-
return function useSyncedState(initialValue, key) {
|
|
19
|
+
return function useSyncedState(initialValue, key, roomId = options.roomId) {
|
|
20
20
|
if (typeof window === "undefined" && !options.hooks) {
|
|
21
21
|
return [initialValue, () => { }];
|
|
22
22
|
}
|
|
23
|
+
const resolvedUrl = roomId ? `${basePath}/${roomId}` : basePath;
|
|
23
24
|
const client = getSyncedStateClient(resolvedUrl);
|
|
24
25
|
const [value, setValue] = useState(initialValue);
|
|
25
26
|
const valueRef = useRef(value);
|
|
@@ -49,7 +50,12 @@ export const createSyncedStateHook = (options = {}) => {
|
|
|
49
50
|
void client.subscribe(key, handleUpdate);
|
|
50
51
|
return () => {
|
|
51
52
|
isActive = false;
|
|
52
|
-
|
|
53
|
+
// Call unsubscribe when component unmounts
|
|
54
|
+
// Page reloads are handled by the beforeunload event listener in client-core.ts
|
|
55
|
+
void client.unsubscribe(key, handleUpdate).catch((error) => {
|
|
56
|
+
// Log but don't throw - cleanup should not prevent unmounting
|
|
57
|
+
console.error("[useSyncedState] Error during unsubscribe:", error);
|
|
58
|
+
});
|
|
53
59
|
};
|
|
54
60
|
}, [client, key, setValue, valueRef]);
|
|
55
61
|
return [value, setSyncValue];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestInfo } from "../runtime/requestInfo/types";
|
|
1
2
|
import { SyncedStateServer } from "./SyncedStateServer.mjs";
|
|
2
3
|
export { SyncedStateServer };
|
|
3
4
|
export type SyncedStateRouteOptions = {
|
|
@@ -10,4 +11,4 @@ export type SyncedStateRouteOptions = {
|
|
|
10
11
|
* @param options Optional overrides for base path and object name.
|
|
11
12
|
* @returns Router entries for the sync state API.
|
|
12
13
|
*/
|
|
13
|
-
export declare const syncedStateRoutes: (getNamespace: (env: Cloudflare.Env) => DurableObjectNamespace<SyncedStateServer>, options?: SyncedStateRouteOptions) => import("../runtime/lib/router.js").RouteDefinition<`/${string}`,
|
|
14
|
+
export declare const syncedStateRoutes: (getNamespace: (env: Cloudflare.Env) => DurableObjectNamespace<SyncedStateServer>, options?: SyncedStateRouteOptions) => import("../runtime/lib/router.js").RouteDefinition<`/${string}`, RequestInfo<any, import("../runtime/requestInfo/types").DefaultAppContext>>[];
|
|
@@ -9,40 +9,91 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
11
|
};
|
|
12
|
-
var _SyncedStateProxy_stub, _SyncedStateProxy_keyHandler;
|
|
12
|
+
var _SyncedStateProxy_instances, _SyncedStateProxy_stub, _SyncedStateProxy_keyHandler, _SyncedStateProxy_requestInfo, _SyncedStateProxy_transformKey, _SyncedStateProxy_callHandler;
|
|
13
13
|
import { RpcTarget, newWorkersRpcResponse } from "capnweb";
|
|
14
14
|
import { env } from "cloudflare:workers";
|
|
15
15
|
import { route } from "../runtime/entries/router";
|
|
16
|
+
import { runWithRequestInfo } from "../runtime/requestInfo/worker";
|
|
16
17
|
import { SyncedStateServer, } from "./SyncedStateServer.mjs";
|
|
17
18
|
import { DEFAULT_SYNCED_STATE_PATH } from "./constants.mjs";
|
|
18
19
|
export { SyncedStateServer };
|
|
19
20
|
const DEFAULT_SYNC_STATE_NAME = "syncedState";
|
|
20
21
|
class SyncedStateProxy extends RpcTarget {
|
|
21
|
-
constructor(stub, keyHandler) {
|
|
22
|
+
constructor(stub, keyHandler, requestInfo) {
|
|
22
23
|
super();
|
|
24
|
+
_SyncedStateProxy_instances.add(this);
|
|
23
25
|
_SyncedStateProxy_stub.set(this, void 0);
|
|
24
26
|
_SyncedStateProxy_keyHandler.set(this, void 0);
|
|
27
|
+
_SyncedStateProxy_requestInfo.set(this, void 0);
|
|
25
28
|
__classPrivateFieldSet(this, _SyncedStateProxy_stub, stub, "f");
|
|
26
29
|
__classPrivateFieldSet(this, _SyncedStateProxy_keyHandler, keyHandler, "f");
|
|
30
|
+
__classPrivateFieldSet(this, _SyncedStateProxy_requestInfo, requestInfo, "f");
|
|
31
|
+
// Set stub in DO instance so handlers can access it
|
|
32
|
+
if (stub && typeof stub._setStub === "function") {
|
|
33
|
+
void stub._setStub(stub);
|
|
34
|
+
}
|
|
27
35
|
}
|
|
28
36
|
async getState(key) {
|
|
29
|
-
const transformedKey = __classPrivateFieldGet(this,
|
|
37
|
+
const transformedKey = await __classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_transformKey).call(this, key);
|
|
30
38
|
return __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").getState(transformedKey);
|
|
31
39
|
}
|
|
32
40
|
async setState(value, key) {
|
|
33
|
-
const transformedKey = __classPrivateFieldGet(this,
|
|
41
|
+
const transformedKey = await __classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_transformKey).call(this, key);
|
|
34
42
|
return __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").setState(value, transformedKey);
|
|
35
43
|
}
|
|
36
44
|
async subscribe(key, client) {
|
|
37
|
-
const transformedKey = __classPrivateFieldGet(this,
|
|
38
|
-
|
|
45
|
+
const transformedKey = await __classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_transformKey).call(this, key);
|
|
46
|
+
const subscribeHandler = SyncedStateServer.getSubscribeHandler();
|
|
47
|
+
if (subscribeHandler) {
|
|
48
|
+
__classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_callHandler).call(this, subscribeHandler, transformedKey, __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f"));
|
|
49
|
+
}
|
|
50
|
+
// dup the client if it is a function; otherwise, pass it as is;
|
|
51
|
+
// this is because the client is a WebSocketRpcSession, and we need to pass a new instance of the client to the DO;
|
|
52
|
+
const clientToPass = typeof client.dup === "function" ? client.dup() : client;
|
|
53
|
+
return __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").subscribe(transformedKey, clientToPass);
|
|
39
54
|
}
|
|
40
55
|
async unsubscribe(key, client) {
|
|
41
|
-
const transformedKey = __classPrivateFieldGet(this,
|
|
42
|
-
|
|
56
|
+
const transformedKey = await __classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_transformKey).call(this, key);
|
|
57
|
+
// Call unsubscribe handler before unsubscribe, similar to subscribe handler
|
|
58
|
+
// This ensures the handler is called even if the unsubscribe doesn't find a match
|
|
59
|
+
// or if the RPC call fails
|
|
60
|
+
const unsubscribeHandler = SyncedStateServer.getUnsubscribeHandler();
|
|
61
|
+
if (unsubscribeHandler) {
|
|
62
|
+
__classPrivateFieldGet(this, _SyncedStateProxy_instances, "m", _SyncedStateProxy_callHandler).call(this, unsubscribeHandler, transformedKey, __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f"));
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f").unsubscribe(transformedKey, client);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// Ignore errors during unsubscribe - handler has already been called
|
|
69
|
+
// This prevents RPC stub disposal errors from propagating
|
|
70
|
+
}
|
|
43
71
|
}
|
|
44
72
|
}
|
|
45
|
-
_SyncedStateProxy_stub = new WeakMap(), _SyncedStateProxy_keyHandler = new WeakMap()
|
|
73
|
+
_SyncedStateProxy_stub = new WeakMap(), _SyncedStateProxy_keyHandler = new WeakMap(), _SyncedStateProxy_requestInfo = new WeakMap(), _SyncedStateProxy_instances = new WeakSet(), _SyncedStateProxy_transformKey =
|
|
74
|
+
/**
|
|
75
|
+
* Transforms a key using the keyHandler, preserving async context so requestInfo.ctx is available.
|
|
76
|
+
*/
|
|
77
|
+
async function _SyncedStateProxy_transformKey(key) {
|
|
78
|
+
if (!__classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f")) {
|
|
79
|
+
return key;
|
|
80
|
+
}
|
|
81
|
+
if (__classPrivateFieldGet(this, _SyncedStateProxy_requestInfo, "f")) {
|
|
82
|
+
// Preserve async context when calling keyHandler so requestInfo.ctx is available
|
|
83
|
+
return await runWithRequestInfo(__classPrivateFieldGet(this, _SyncedStateProxy_requestInfo, "f"), async () => await __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f")(key, __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f")));
|
|
84
|
+
}
|
|
85
|
+
return await __classPrivateFieldGet(this, _SyncedStateProxy_keyHandler, "f").call(this, key, __classPrivateFieldGet(this, _SyncedStateProxy_stub, "f"));
|
|
86
|
+
}, _SyncedStateProxy_callHandler = function _SyncedStateProxy_callHandler(handler, key, stub) {
|
|
87
|
+
if (__classPrivateFieldGet(this, _SyncedStateProxy_requestInfo, "f")) {
|
|
88
|
+
// Preserve async context when calling handler so requestInfo.ctx is available
|
|
89
|
+
runWithRequestInfo(__classPrivateFieldGet(this, _SyncedStateProxy_requestInfo, "f"), () => {
|
|
90
|
+
handler(key, stub);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
handler(key, stub);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
46
97
|
/**
|
|
47
98
|
* Registers routes that forward sync state requests to the configured Durable Object namespace.
|
|
48
99
|
* @param getNamespace Function that returns the Durable Object namespace from the Worker env.
|
|
@@ -52,18 +103,33 @@ _SyncedStateProxy_stub = new WeakMap(), _SyncedStateProxy_keyHandler = new WeakM
|
|
|
52
103
|
export const syncedStateRoutes = (getNamespace, options = {}) => {
|
|
53
104
|
const basePath = options.basePath ?? DEFAULT_SYNCED_STATE_PATH;
|
|
54
105
|
const durableObjectName = options.durableObjectName ?? DEFAULT_SYNC_STATE_NAME;
|
|
55
|
-
const forwardRequest = async (request) => {
|
|
106
|
+
const forwardRequest = async (request, requestInfo) => {
|
|
107
|
+
const namespace = getNamespace(env);
|
|
108
|
+
// Register the namespace and DO name so handlers can access it
|
|
109
|
+
SyncedStateServer.registerNamespace(namespace, durableObjectName);
|
|
56
110
|
const keyHandler = SyncedStateServer.getKeyHandler();
|
|
111
|
+
const roomHandler = SyncedStateServer.getRoomHandler();
|
|
112
|
+
// Get the room ID from the URL parameter, or undefined if not present
|
|
113
|
+
const idParam = requestInfo.params?.id;
|
|
114
|
+
// Resolve the room name using the roomHandler if present, otherwise use the param or default
|
|
115
|
+
let resolvedRoomName;
|
|
116
|
+
if (roomHandler) {
|
|
117
|
+
resolvedRoomName = await runWithRequestInfo(requestInfo, async () => await roomHandler(idParam, requestInfo));
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
resolvedRoomName = idParam ?? durableObjectName;
|
|
121
|
+
}
|
|
57
122
|
if (!keyHandler) {
|
|
58
|
-
const
|
|
59
|
-
const id = namespace.idFromName(durableObjectName);
|
|
123
|
+
const id = namespace.idFromName(resolvedRoomName);
|
|
60
124
|
return namespace.get(id).fetch(request);
|
|
61
125
|
}
|
|
62
|
-
const
|
|
63
|
-
const id = namespace.idFromName(durableObjectName);
|
|
126
|
+
const id = namespace.idFromName(resolvedRoomName);
|
|
64
127
|
const coordinator = namespace.get(id);
|
|
65
|
-
const proxy = new SyncedStateProxy(coordinator, keyHandler);
|
|
128
|
+
const proxy = new SyncedStateProxy(coordinator, keyHandler, requestInfo);
|
|
66
129
|
return newWorkersRpcResponse(request, proxy);
|
|
67
130
|
};
|
|
68
|
-
return [
|
|
131
|
+
return [
|
|
132
|
+
route(basePath, (requestInfo) => forwardRequest(requestInfo.request, requestInfo)),
|
|
133
|
+
route(basePath + "/:id", (requestInfo) => forwardRequest(requestInfo.request, requestInfo)),
|
|
134
|
+
];
|
|
69
135
|
};
|
|
@@ -298,7 +298,9 @@ export const runDirectivesScan = async ({ rootConfig, environments, clientFiles,
|
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
300
|
catch (e) {
|
|
301
|
-
throw new Error(`
|
|
301
|
+
throw new Error(`RedwoodSDK: Directive scan failed. This often happens due to syntax errors in files using "use client" or "use server". Check your directive files for issues.\n\n` +
|
|
302
|
+
`For detailed troubleshooting steps, see: https://docs.rwsdk.com/guides/troubleshooting#directive-scan-errors\n\n` +
|
|
303
|
+
`${e.stack}`);
|
|
302
304
|
}
|
|
303
305
|
finally {
|
|
304
306
|
deferredLog("✔ (rwsdk) Done scanning for 'use client' and 'use server' directives.");
|
|
@@ -288,6 +288,7 @@ function SidebarGroup() { return jsx("div", {}); }
|
|
|
288
288
|
function SidebarGroupLabel() { return jsx("div", {}); }
|
|
289
289
|
function SidebarGroupAction() { return jsx("div", {}); }
|
|
290
290
|
function SidebarGroupContent() { return jsx("div", {}); }
|
|
291
|
+
function SidebarGroupContent() { return jsx("div", {}); }
|
|
291
292
|
function SidebarMenu() { return jsx("div", {}); }
|
|
292
293
|
function SidebarMenuItem() { return jsx("div", {}); }
|
|
293
294
|
function SidebarMenuButton() { return jsx("div", {}); }
|
|
@@ -326,6 +327,37 @@ const SidebarSeparator = registerClientReference(SSRModule, "/test/file.tsx", "S
|
|
|
326
327
|
const SidebarTrigger = registerClientReference(SSRModule, "/test/file.tsx", "SidebarTrigger");
|
|
327
328
|
const useSidebar = registerClientReference(SSRModule, "/test/file.tsx", "useSidebar");
|
|
328
329
|
export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, useSidebar };
|
|
330
|
+
`);
|
|
331
|
+
});
|
|
332
|
+
it("does not transform inlined functions (Issue #471)", async () => {
|
|
333
|
+
const code = `"use client"
|
|
334
|
+
|
|
335
|
+
export function Stars({ level }) {
|
|
336
|
+
const renderStars = (level) => {
|
|
337
|
+
return level;
|
|
338
|
+
}
|
|
339
|
+
return renderStars(level);
|
|
340
|
+
}`;
|
|
341
|
+
expect((await transform(code)) ?? "").toEqual(`import { ssrLoadModule } from "rwsdk/__ssr_bridge";
|
|
342
|
+
import { registerClientReference } from "rwsdk/worker";
|
|
343
|
+
const SSRModule = await ssrLoadModule("/test/file.tsx");
|
|
344
|
+
const Stars = registerClientReference(SSRModule, "/test/file.tsx", "Stars");
|
|
345
|
+
export { Stars };
|
|
346
|
+
`);
|
|
347
|
+
});
|
|
348
|
+
it("does not transform inlined functions in default export (Issue #471)", async () => {
|
|
349
|
+
const code = `"use client"
|
|
350
|
+
|
|
351
|
+
export default function Stars({ level }) {
|
|
352
|
+
const renderStars = (level) => {
|
|
353
|
+
return level;
|
|
354
|
+
}
|
|
355
|
+
return renderStars(level);
|
|
356
|
+
}`;
|
|
357
|
+
expect((await transform(code)) ?? "").toEqual(`import { ssrLoadModule } from "rwsdk/__ssr_bridge";
|
|
358
|
+
import { registerClientReference } from "rwsdk/worker";
|
|
359
|
+
const SSRModule = await ssrLoadModule("/test/file.tsx");
|
|
360
|
+
export default registerClientReference(SSRModule, "/test/file.tsx", "default");
|
|
329
361
|
`);
|
|
330
362
|
});
|
|
331
363
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rwsdk",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.44",
|
|
4
4
|
"description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,13 +18,16 @@
|
|
|
18
18
|
"build:watch": "npm run build -- --watch",
|
|
19
19
|
"release": "./scripts/release.sh",
|
|
20
20
|
"test": "vitest --run",
|
|
21
|
+
"bench": "vitest bench --run",
|
|
22
|
+
"bench:baseline": "vitest bench --run --outputJson=benchmarks/router-bench-baseline.json",
|
|
23
|
+
"bench:compare": "vitest bench --run --compare=benchmarks/router-bench-baseline.json",
|
|
21
24
|
"debug:sync": "tsx ./src/scripts/debug-sync.mts",
|
|
22
25
|
"smoke-test": "tsx ./src/scripts/smoke-test.mts"
|
|
23
26
|
},
|
|
24
27
|
"exports": {
|
|
25
28
|
"./vite": {
|
|
26
|
-
"
|
|
27
|
-
"
|
|
29
|
+
"types": "./dist/vite/index.d.mts",
|
|
30
|
+
"default": "./dist/vite/index.mjs"
|
|
28
31
|
},
|
|
29
32
|
"./worker": {
|
|
30
33
|
"react-server": "./dist/runtime/entries/worker.js",
|
|
@@ -179,6 +182,7 @@
|
|
|
179
182
|
"puppeteer-core": "~24.22.0",
|
|
180
183
|
"react-is": "~19.1.0",
|
|
181
184
|
"rsc-html-stream": "~0.0.6",
|
|
185
|
+
"server-only": "^0.0.1",
|
|
182
186
|
"tmp-promise": "~3.0.3",
|
|
183
187
|
"ts-morph": "~27.0.0",
|
|
184
188
|
"unique-names-generator": "~4.7.1",
|