rwsdk 1.0.0-beta.42 → 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.
- package/dist/runtime/client/client.d.ts +22 -1
- package/dist/runtime/client/client.js +22 -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/transformClientComponents.test.mjs +32 -0
- package/package.json +6 -3
|
@@ -13,7 +13,11 @@ export declare const fetchTransport: Transport;
|
|
|
13
13
|
* making the page interactive. Call this from your client entry point.
|
|
14
14
|
*
|
|
15
15
|
* @param transport - Custom transport for server communication (defaults to fetchTransport)
|
|
16
|
-
* @param hydrateRootOptions - Options passed to React's hydrateRoot
|
|
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
|
|
17
21
|
* @param handleResponse - Custom response handler for navigation errors (navigation GETs)
|
|
18
22
|
* @param onHydrationUpdate - Callback invoked after a new RSC payload has been committed on the client
|
|
19
23
|
* @param onActionResponse - Optional hook invoked when an action returns a Response;
|
|
@@ -34,6 +38,23 @@ export declare const fetchTransport: Transport;
|
|
|
34
38
|
* initClient({ handleResponse });
|
|
35
39
|
*
|
|
36
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
|
|
37
58
|
* // With custom React hydration options
|
|
38
59
|
* initClient({
|
|
39
60
|
* hydrateRootOptions: {
|
|
@@ -105,7 +105,11 @@ export const fetchTransport = (transportContext) => {
|
|
|
105
105
|
* making the page interactive. Call this from your client entry point.
|
|
106
106
|
*
|
|
107
107
|
* @param transport - Custom transport for server communication (defaults to fetchTransport)
|
|
108
|
-
* @param hydrateRootOptions - Options passed to React's hydrateRoot
|
|
108
|
+
* @param hydrateRootOptions - Options passed to React's `hydrateRoot`. Supports all React hydration options including:
|
|
109
|
+
* - `onUncaughtError`: Handler for uncaught errors (async errors, event handler errors).
|
|
110
|
+
* If not provided, defaults to logging errors to console.
|
|
111
|
+
* - `onCaughtError`: Handler for errors caught by error boundaries
|
|
112
|
+
* - `onRecoverableError`: Handler for recoverable errors
|
|
109
113
|
* @param handleResponse - Custom response handler for navigation errors (navigation GETs)
|
|
110
114
|
* @param onHydrationUpdate - Callback invoked after a new RSC payload has been committed on the client
|
|
111
115
|
* @param onActionResponse - Optional hook invoked when an action returns a Response;
|
|
@@ -126,6 +130,23 @@ export const fetchTransport = (transportContext) => {
|
|
|
126
130
|
* initClient({ handleResponse });
|
|
127
131
|
*
|
|
128
132
|
* @example
|
|
133
|
+
* // With error handling
|
|
134
|
+
* initClient({
|
|
135
|
+
* hydrateRootOptions: {
|
|
136
|
+
* onUncaughtError: (error, errorInfo) => {
|
|
137
|
+
* console.error("Uncaught error:", error);
|
|
138
|
+
* // Send to monitoring service
|
|
139
|
+
* sendToSentry(error, errorInfo);
|
|
140
|
+
* },
|
|
141
|
+
* onCaughtError: (error, errorInfo) => {
|
|
142
|
+
* console.error("Caught error:", error);
|
|
143
|
+
* // Handle errors from error boundaries
|
|
144
|
+
* sendToSentry(error, errorInfo);
|
|
145
|
+
* },
|
|
146
|
+
* },
|
|
147
|
+
* });
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
129
150
|
* // With custom React hydration options
|
|
130
151
|
* initClient({
|
|
131
152
|
* hydrateRootOptions: {
|
|
@@ -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:
|
|
12
|
-
static getKeyHandler():
|
|
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;
|
|
@@ -9,7 +9,7 @@ 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 _a, _SyncedStateServer_keyHandler, _SyncedStateServer_setStateHandler, _SyncedStateServer_getStateHandler, _SyncedStateServer_stateStore, _SyncedStateServer_subscriptions, _SyncedStateServer_subscriptionRefs, _CoordinatorApi_coordinator;
|
|
12
|
+
var _SyncedStateServer_instances, _a, _SyncedStateServer_keyHandler, _SyncedStateServer_roomHandler, _SyncedStateServer_setStateHandler, _SyncedStateServer_getStateHandler, _SyncedStateServer_subscribeHandler, _SyncedStateServer_unsubscribeHandler, _SyncedStateServer_namespace, _SyncedStateServer_durableObjectName, _SyncedStateServer_stub, _SyncedStateServer_stateStore, _SyncedStateServer_subscriptions, _SyncedStateServer_subscriptionRefs, _SyncedStateServer_getStubForHandlers, _CoordinatorApi_coordinator, _CoordinatorApi_stub;
|
|
13
13
|
import { RpcTarget, newWorkersRpcResponse } from "capnweb";
|
|
14
14
|
import { DurableObject } from "cloudflare:workers";
|
|
15
15
|
/**
|
|
@@ -18,6 +18,8 @@ import { DurableObject } from "cloudflare:workers";
|
|
|
18
18
|
export class SyncedStateServer extends DurableObject {
|
|
19
19
|
constructor() {
|
|
20
20
|
super(...arguments);
|
|
21
|
+
_SyncedStateServer_instances.add(this);
|
|
22
|
+
_SyncedStateServer_stub.set(this, null);
|
|
21
23
|
_SyncedStateServer_stateStore.set(this, new Map());
|
|
22
24
|
_SyncedStateServer_subscriptions.set(this, new Map());
|
|
23
25
|
_SyncedStateServer_subscriptionRefs.set(this, new Map());
|
|
@@ -28,23 +30,62 @@ export class SyncedStateServer extends DurableObject {
|
|
|
28
30
|
static getKeyHandler() {
|
|
29
31
|
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_keyHandler);
|
|
30
32
|
}
|
|
33
|
+
static registerRoomHandler(handler) {
|
|
34
|
+
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_roomHandler);
|
|
35
|
+
}
|
|
36
|
+
static getRoomHandler() {
|
|
37
|
+
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_roomHandler);
|
|
38
|
+
}
|
|
39
|
+
static registerNamespace(namespace, durableObjectName) {
|
|
40
|
+
__classPrivateFieldSet(_a, _a, namespace, "f", _SyncedStateServer_namespace);
|
|
41
|
+
if (durableObjectName) {
|
|
42
|
+
__classPrivateFieldSet(_a, _a, durableObjectName, "f", _SyncedStateServer_durableObjectName);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
static getNamespace() {
|
|
46
|
+
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_namespace);
|
|
47
|
+
}
|
|
48
|
+
static getDurableObjectName() {
|
|
49
|
+
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_durableObjectName);
|
|
50
|
+
}
|
|
51
|
+
setStub(stub) {
|
|
52
|
+
__classPrivateFieldSet(this, _SyncedStateServer_stub, stub, "f");
|
|
53
|
+
}
|
|
31
54
|
static registerSetStateHandler(handler) {
|
|
32
55
|
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_setStateHandler);
|
|
33
56
|
}
|
|
34
57
|
static registerGetStateHandler(handler) {
|
|
35
58
|
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_getStateHandler);
|
|
36
59
|
}
|
|
60
|
+
static registerSubscribeHandler(handler) {
|
|
61
|
+
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_subscribeHandler);
|
|
62
|
+
}
|
|
63
|
+
static registerUnsubscribeHandler(handler) {
|
|
64
|
+
__classPrivateFieldSet(_a, _a, handler, "f", _SyncedStateServer_unsubscribeHandler);
|
|
65
|
+
}
|
|
66
|
+
static getSubscribeHandler() {
|
|
67
|
+
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_subscribeHandler);
|
|
68
|
+
}
|
|
69
|
+
static getUnsubscribeHandler() {
|
|
70
|
+
return __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_unsubscribeHandler);
|
|
71
|
+
}
|
|
37
72
|
getState(key) {
|
|
38
73
|
const value = __classPrivateFieldGet(this, _SyncedStateServer_stateStore, "f").get(key);
|
|
39
74
|
if (__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_getStateHandler)) {
|
|
40
|
-
__classPrivateFieldGet(
|
|
75
|
+
const stub = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getStubForHandlers).call(this);
|
|
76
|
+
if (stub) {
|
|
77
|
+
__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_getStateHandler).call(_a, key, value, stub);
|
|
78
|
+
}
|
|
41
79
|
}
|
|
42
80
|
return value;
|
|
43
81
|
}
|
|
44
82
|
setState(value, key) {
|
|
45
83
|
__classPrivateFieldGet(this, _SyncedStateServer_stateStore, "f").set(key, value);
|
|
46
84
|
if (__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_setStateHandler)) {
|
|
47
|
-
__classPrivateFieldGet(
|
|
85
|
+
const stub = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getStubForHandlers).call(this);
|
|
86
|
+
if (stub) {
|
|
87
|
+
__classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_setStateHandler).call(_a, key, value, stub);
|
|
88
|
+
}
|
|
48
89
|
}
|
|
49
90
|
const subscribers = __classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").get(key);
|
|
50
91
|
if (!subscribers) {
|
|
@@ -78,7 +119,9 @@ export class SyncedStateServer extends DurableObject {
|
|
|
78
119
|
if (!__classPrivateFieldGet(this, _SyncedStateServer_subscriptionRefs, "f").has(key)) {
|
|
79
120
|
__classPrivateFieldGet(this, _SyncedStateServer_subscriptionRefs, "f").set(key, new Map());
|
|
80
121
|
}
|
|
81
|
-
const duplicate = client.dup
|
|
122
|
+
const duplicate = typeof client.dup === "function"
|
|
123
|
+
? client.dup()
|
|
124
|
+
: client;
|
|
82
125
|
__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").get(key).add(duplicate);
|
|
83
126
|
__classPrivateFieldGet(this, _SyncedStateServer_subscriptionRefs, "f").get(key).set(client, duplicate);
|
|
84
127
|
}
|
|
@@ -98,19 +141,44 @@ export class SyncedStateServer extends DurableObject {
|
|
|
98
141
|
}
|
|
99
142
|
}
|
|
100
143
|
async fetch(request) {
|
|
101
|
-
|
|
144
|
+
// Create a placeholder stub - it will be set by the worker via _setStub
|
|
145
|
+
const api = new CoordinatorApi(this, __classPrivateFieldGet(this, _SyncedStateServer_stub, "f") || {});
|
|
102
146
|
return newWorkersRpcResponse(request, api);
|
|
103
147
|
}
|
|
104
148
|
}
|
|
105
|
-
_a = SyncedStateServer, _SyncedStateServer_stateStore = new WeakMap(), _SyncedStateServer_subscriptions = new WeakMap(), _SyncedStateServer_subscriptionRefs = new WeakMap()
|
|
149
|
+
_a = SyncedStateServer, _SyncedStateServer_stub = new WeakMap(), _SyncedStateServer_stateStore = new WeakMap(), _SyncedStateServer_subscriptions = new WeakMap(), _SyncedStateServer_subscriptionRefs = new WeakMap(), _SyncedStateServer_instances = new WeakSet(), _SyncedStateServer_getStubForHandlers = function _SyncedStateServer_getStubForHandlers() {
|
|
150
|
+
// If we have a stub already, use it
|
|
151
|
+
if (__classPrivateFieldGet(this, _SyncedStateServer_stub, "f")) {
|
|
152
|
+
return __classPrivateFieldGet(this, _SyncedStateServer_stub, "f");
|
|
153
|
+
}
|
|
154
|
+
// Otherwise, try to get a stub from the registered namespace using our own ID
|
|
155
|
+
const namespace = __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_namespace);
|
|
156
|
+
if (namespace) {
|
|
157
|
+
return namespace.get(this.ctx.id);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
106
161
|
_SyncedStateServer_keyHandler = { value: null };
|
|
162
|
+
_SyncedStateServer_roomHandler = { value: null };
|
|
107
163
|
_SyncedStateServer_setStateHandler = { value: null };
|
|
108
164
|
_SyncedStateServer_getStateHandler = { value: null };
|
|
165
|
+
_SyncedStateServer_subscribeHandler = { value: null };
|
|
166
|
+
_SyncedStateServer_unsubscribeHandler = { value: null };
|
|
167
|
+
_SyncedStateServer_namespace = { value: null };
|
|
168
|
+
_SyncedStateServer_durableObjectName = { value: "syncedState" };
|
|
109
169
|
class CoordinatorApi extends RpcTarget {
|
|
110
|
-
constructor(coordinator) {
|
|
170
|
+
constructor(coordinator, stub) {
|
|
111
171
|
super();
|
|
112
172
|
_CoordinatorApi_coordinator.set(this, void 0);
|
|
173
|
+
_CoordinatorApi_stub.set(this, void 0);
|
|
113
174
|
__classPrivateFieldSet(this, _CoordinatorApi_coordinator, coordinator, "f");
|
|
175
|
+
__classPrivateFieldSet(this, _CoordinatorApi_stub, stub, "f");
|
|
176
|
+
coordinator.setStub(stub);
|
|
177
|
+
}
|
|
178
|
+
// Internal method to set the stub - called from worker
|
|
179
|
+
_setStub(stub) {
|
|
180
|
+
__classPrivateFieldSet(this, _CoordinatorApi_stub, stub, "f");
|
|
181
|
+
__classPrivateFieldGet(this, _CoordinatorApi_coordinator, "f").setStub(stub);
|
|
114
182
|
}
|
|
115
183
|
getState(key) {
|
|
116
184
|
return __classPrivateFieldGet(this, _CoordinatorApi_coordinator, "f").getState(key);
|
|
@@ -125,4 +193,4 @@ class CoordinatorApi extends RpcTarget {
|
|
|
125
193
|
__classPrivateFieldGet(this, _CoordinatorApi_coordinator, "f").unsubscribe(key, client);
|
|
126
194
|
}
|
|
127
195
|
}
|
|
128
|
-
_CoordinatorApi_coordinator = new WeakMap();
|
|
196
|
+
_CoordinatorApi_coordinator = new WeakMap(), _CoordinatorApi_stub = new WeakMap();
|
|
@@ -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
|
};
|
|
@@ -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.43",
|
|
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",
|