rwsdk 1.5.1 → 1.5.2
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/use-synced-state/hibernation/__tests__/client-core.test.js +116 -30
- package/dist/use-synced-state/hibernation/__tests__/server.test.mjs +231 -41
- package/dist/use-synced-state/hibernation/client-core.d.ts +1 -1
- package/dist/use-synced-state/hibernation/client-core.js +2 -2
- package/dist/use-synced-state/hibernation/connection/connection.js +5 -2
- package/dist/use-synced-state/hibernation/connection/messages.js +5 -0
- package/dist/use-synced-state/hibernation/connection/timer.d.ts +3 -2
- package/dist/use-synced-state/hibernation/connection/timer.js +25 -6
- package/dist/use-synced-state/hibernation/connection/types.d.ts +1 -1
- package/dist/use-synced-state/hibernation/identity.d.mts +3 -0
- package/dist/use-synced-state/hibernation/identity.mjs +21 -2
- package/dist/use-synced-state/hibernation/server.d.mts +1 -1
- package/dist/use-synced-state/hibernation/server.mjs +87 -140
- package/dist/use-synced-state/hibernation/state/clientFactory.js +28 -13
- package/dist/use-synced-state/hibernation/state/clientManager.js +6 -4
- package/dist/use-synced-state/hibernation/worker.mjs +25 -7
- package/dist/vite/configPlugin.mjs +8 -4
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { packMessage, unpackServerMessage, } from "../protocol.mjs";
|
|
2
|
+
import { startPendingRequestTimer } from "./timer.js";
|
|
2
3
|
export function makeMessageId(connection) {
|
|
3
4
|
return `${connection.nextId++}`;
|
|
4
5
|
}
|
|
@@ -7,6 +8,7 @@ export async function sendMessage(connection, message) {
|
|
|
7
8
|
if (isOpen) {
|
|
8
9
|
return new Promise((resolve, reject) => {
|
|
9
10
|
pending.set(message.id, { resolve, reject });
|
|
11
|
+
startPendingRequestTimer(connection);
|
|
10
12
|
ws.send(packMessage(message));
|
|
11
13
|
});
|
|
12
14
|
}
|
|
@@ -14,6 +16,7 @@ export async function sendMessage(connection, message) {
|
|
|
14
16
|
const onOpen = () => {
|
|
15
17
|
cleanup();
|
|
16
18
|
connection.pending.set(message.id, { resolve, reject });
|
|
19
|
+
startPendingRequestTimer(connection);
|
|
17
20
|
connection.ws.send(packMessage(message));
|
|
18
21
|
};
|
|
19
22
|
const onClose = () => {
|
|
@@ -44,6 +47,7 @@ export function handleServerMessage(connection, message) {
|
|
|
44
47
|
pending.reject(new Error(message.message));
|
|
45
48
|
}
|
|
46
49
|
}
|
|
50
|
+
startPendingRequestTimer(connection);
|
|
47
51
|
return;
|
|
48
52
|
}
|
|
49
53
|
const pending = connection.pending.get(message.id);
|
|
@@ -51,6 +55,7 @@ export function handleServerMessage(connection, message) {
|
|
|
51
55
|
return;
|
|
52
56
|
connection.pending.delete(message.id);
|
|
53
57
|
pending.resolve(message.kind === "getState" ? message.value : undefined);
|
|
58
|
+
startPendingRequestTimer(connection);
|
|
54
59
|
}
|
|
55
60
|
export function unpackMessage(data) {
|
|
56
61
|
if (typeof data !== "string")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Connection } from "./types.js";
|
|
2
|
-
export declare const
|
|
3
|
-
export declare function
|
|
2
|
+
export declare const PENDING_REQUEST_TIMEOUT_MS = 30000;
|
|
3
|
+
export declare function startPendingRequestTimer(connection: Connection): void;
|
|
4
|
+
export declare function stopPendingRequestTimer(connection: Connection): void;
|
|
4
5
|
export declare function rejectPending(connection: Connection, reason: string): void;
|
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// context(justinvdm, 29 Jun 2026): This timeout applies only to requests that
|
|
2
|
+
// are waiting for a server response, not to idle connections. Hibernation
|
|
3
|
+
// relies on idle sockets being allowed to sleep, so we must not close a socket
|
|
4
|
+
// just because no traffic has arrived.
|
|
5
|
+
export const PENDING_REQUEST_TIMEOUT_MS = 30_000;
|
|
6
|
+
export function startPendingRequestTimer(connection) {
|
|
7
|
+
stopPendingRequestTimer(connection);
|
|
8
|
+
if (connection.pending.size === 0) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
connection.pendingRequestTimer = setTimeout(() => {
|
|
12
|
+
// We intentionally do not close the socket here. In Cloudflare's
|
|
13
|
+
// environment a half-open WebSocket is unlikely; the server should
|
|
14
|
+
// already have sent an error frame for any throw (see server.mts).
|
|
15
|
+
// Closing would trigger a visible reconnect that looks like the old
|
|
16
|
+
// idle-timeout symptom. We only reject the pending promises so the
|
|
17
|
+
// caller is not left hanging forever.
|
|
18
|
+
rejectPending(connection, "useSyncedState request timed out");
|
|
19
|
+
}, PENDING_REQUEST_TIMEOUT_MS);
|
|
20
|
+
}
|
|
21
|
+
export function stopPendingRequestTimer(connection) {
|
|
22
|
+
if (connection.pendingRequestTimer) {
|
|
23
|
+
clearTimeout(connection.pendingRequestTimer);
|
|
24
|
+
connection.pendingRequestTimer = null;
|
|
6
25
|
}
|
|
7
26
|
}
|
|
8
27
|
export function rejectPending(connection, reason) {
|
|
@@ -10,5 +29,5 @@ export function rejectPending(connection, reason) {
|
|
|
10
29
|
pending.reject(new Error(reason));
|
|
11
30
|
}
|
|
12
31
|
connection.pending.clear();
|
|
13
|
-
|
|
32
|
+
stopPendingRequestTimer(connection);
|
|
14
33
|
}
|
|
@@ -17,6 +17,6 @@ export type Connection = {
|
|
|
17
17
|
pending: Map<string, PendingRequest>;
|
|
18
18
|
isOpen: boolean;
|
|
19
19
|
messageHandlers: Map<string, Set<(value: unknown) => void>>;
|
|
20
|
-
|
|
20
|
+
pendingRequestTimer: ReturnType<typeof setTimeout> | null;
|
|
21
21
|
webSocketFactory: WebSocketFactory;
|
|
22
22
|
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export type SyncedStateIdentity = unknown;
|
|
2
|
+
export declare class SyncedStateIdentityError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
2
5
|
export declare function setIdentityInUrl(identity: SyncedStateIdentity, url: URL): URL;
|
|
3
6
|
export declare function getIdentityFromUrl(url: URL): SyncedStateIdentity;
|
|
4
7
|
export declare function removeIdentityFromUrl(url: URL): URL;
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
const IDENTITY_QUERY_PARAM = "__ssi";
|
|
2
|
-
|
|
2
|
+
const MAX_IDENTITY_SIZE_BYTES = 4096;
|
|
3
|
+
export class SyncedStateIdentityError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "SyncedStateIdentityError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
// context(justinvdm, 29 Jun 2026): Serialize the captured identity into a URL
|
|
3
10
|
// query parameter so the worker can pass it to the DO during the WebSocket
|
|
4
11
|
// upgrade. The identity is extracted from requestInfo in the worker and is not
|
|
5
12
|
// user-controlled, so passing it in the internal worker->DO request is safe.
|
|
13
|
+
// We validate serializability and size here so a bad extractor fails the
|
|
14
|
+
// handshake with a clear error instead of an opaque internal failure.
|
|
6
15
|
export function setIdentityInUrl(identity, url) {
|
|
7
|
-
|
|
16
|
+
let serialized;
|
|
17
|
+
try {
|
|
18
|
+
serialized = JSON.stringify(identity);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new SyncedStateIdentityError(`useSyncedState identity must be JSON-serializable: ${error instanceof Error ? error.message : String(error)}`);
|
|
22
|
+
}
|
|
23
|
+
if (serialized.length > MAX_IDENTITY_SIZE_BYTES) {
|
|
24
|
+
throw new SyncedStateIdentityError(`useSyncedState identity exceeds maximum size of ${MAX_IDENTITY_SIZE_BYTES} bytes`);
|
|
25
|
+
}
|
|
26
|
+
url.searchParams.set(IDENTITY_QUERY_PARAM, serialized);
|
|
8
27
|
return url;
|
|
9
28
|
}
|
|
10
29
|
export function getIdentityFromUrl(url) {
|
|
@@ -55,7 +55,7 @@ export declare class SyncedStateServer extends DurableObject {
|
|
|
55
55
|
setStub(stub: DurableObjectStub<SyncedStateServer>): void;
|
|
56
56
|
fetch(request: Request): Promise<Response>;
|
|
57
57
|
webSocketMessage(ws: WebSocket, data: string | ArrayBuffer): Promise<void>;
|
|
58
|
-
webSocketClose(
|
|
58
|
+
webSocketClose(_ws: WebSocket): Promise<void>;
|
|
59
59
|
getState(key: string): Promise<SyncedStateValue | undefined>;
|
|
60
60
|
setState(value: SyncedStateValue, key: string): Promise<void>;
|
|
61
61
|
}
|
|
@@ -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 _SyncedStateServer_instances, _a, _SyncedStateServer_keyHandler, _SyncedStateServer_roomHandler, _SyncedStateServer_setStateHandler, _SyncedStateServer_getStateHandler, _SyncedStateServer_subscribeHandler, _SyncedStateServer_unsubscribeHandler, _SyncedStateServer_identityExtractor, _SyncedStateServer_namespace, _SyncedStateServer_durableObjectName, _SyncedStateServer_stub, _SyncedStateServer_stateStore, _SyncedStateServer_stateStoreLoaded, _SyncedStateServer_loadStateStore, _SyncedStateServer_stateStorageKey, _SyncedStateServer_getState, _SyncedStateServer_setState,
|
|
12
|
+
var _SyncedStateServer_instances, _a, _SyncedStateServer_keyHandler, _SyncedStateServer_roomHandler, _SyncedStateServer_setStateHandler, _SyncedStateServer_getStateHandler, _SyncedStateServer_subscribeHandler, _SyncedStateServer_unsubscribeHandler, _SyncedStateServer_identityExtractor, _SyncedStateServer_namespace, _SyncedStateServer_durableObjectName, _SyncedStateServer_stub, _SyncedStateServer_handleClientMessage, _SyncedStateServer_stateStore, _SyncedStateServer_stateStoreLoaded, _SyncedStateServer_loadStateStore, _SyncedStateServer_stateStorageKey, _SyncedStateServer_getState, _SyncedStateServer_setState, _SyncedStateServer_subscribe, _SyncedStateServer_unsubscribe, _SyncedStateServer_broadcastUpdate, _SyncedStateServer_getAttachment, _SyncedStateServer_getIdentity, _SyncedStateServer_getSubscriptionsFromAttachment, _SyncedStateServer_setSubscriptionsInAttachment, _SyncedStateServer_resolveStorageKey, _SyncedStateServer_getStubForHandlers, _SyncedStateServer_send, _SyncedStateServer_sendError;
|
|
13
13
|
import { DurableObject } from "cloudflare:workers";
|
|
14
14
|
import { getIdentityFromUrl, } from "./identity.mjs";
|
|
15
15
|
import { unpackClientMessage, packMessage, } from "./protocol.mjs";
|
|
@@ -87,13 +87,6 @@ export class SyncedStateServer extends DurableObject {
|
|
|
87
87
|
// the DO, so every write is persisted and the cache is warmed on first read.
|
|
88
88
|
_SyncedStateServer_stateStore.set(this, new Map());
|
|
89
89
|
_SyncedStateServer_stateStoreLoaded.set(this, false);
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
// Subscriptions
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// Map from storage key to the subscribers for that key. We store the
|
|
94
|
-
// user-facing key per subscriber so broadcasts can send each socket the key
|
|
95
|
-
// it originally subscribed to.
|
|
96
|
-
_SyncedStateServer_subscriptions.set(this, new Map());
|
|
97
90
|
this.state = state;
|
|
98
91
|
this.env = env;
|
|
99
92
|
this.storage = state.storage;
|
|
@@ -128,73 +121,20 @@ export class SyncedStateServer extends DurableObject {
|
|
|
128
121
|
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_sendError).call(this, ws, error instanceof Error ? error.message : "Invalid protocol message");
|
|
129
122
|
return;
|
|
130
123
|
}
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const identity = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getIdentity).call(this, ws);
|
|
136
|
-
const storageKey = await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_resolveStorageKey).call(this, message.key, identity);
|
|
137
|
-
switch (message.kind) {
|
|
138
|
-
case "getState": {
|
|
139
|
-
const value = await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getState).call(this, storageKey, identity);
|
|
140
|
-
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
|
|
141
|
-
kind: "getState",
|
|
142
|
-
key: message.key,
|
|
143
|
-
value,
|
|
144
|
-
id: message.id,
|
|
145
|
-
});
|
|
146
|
-
break;
|
|
147
|
-
}
|
|
148
|
-
case "setState": {
|
|
149
|
-
await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_setState).call(this, storageKey, message.value, identity);
|
|
150
|
-
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
|
|
151
|
-
kind: "setState",
|
|
152
|
-
key: message.key,
|
|
153
|
-
id: message.id,
|
|
154
|
-
});
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
case "subscribe": {
|
|
158
|
-
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_subscribe).call(this, ws, storageKey, message.key);
|
|
159
|
-
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
|
|
160
|
-
kind: "subscribe",
|
|
161
|
-
key: message.key,
|
|
162
|
-
id: message.id,
|
|
163
|
-
});
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
case "unsubscribe": {
|
|
167
|
-
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_unsubscribe).call(this, ws, storageKey, message.key);
|
|
168
|
-
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
|
|
169
|
-
kind: "unsubscribe",
|
|
170
|
-
key: message.key,
|
|
171
|
-
id: message.id,
|
|
172
|
-
});
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
default: {
|
|
176
|
-
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_sendError).call(this, ws, "Unknown message kind", message.id);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async webSocketClose(ws) {
|
|
181
|
-
// context(justinvdm, 18 Jun 2026): Remove this socket from all in-memory
|
|
182
|
-
// subscription sets. The attachment is dropped by the runtime, so no
|
|
183
|
-
// persistent cleanup is required.
|
|
184
|
-
for (const subscribers of __classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").values()) {
|
|
185
|
-
for (const entry of subscribers) {
|
|
186
|
-
if (entry.ws === ws) {
|
|
187
|
-
subscribers.delete(entry);
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
124
|
+
// Once a request id is parsed, the client is waiting for a matching
|
|
125
|
+
// response. Every dispatch path must resolve or reject that request id.
|
|
126
|
+
try {
|
|
127
|
+
await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_handleClientMessage).call(this, ws, message);
|
|
191
128
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").delete(key);
|
|
195
|
-
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_sendError).call(this, ws, error instanceof Error ? error.message : String(error), message.id);
|
|
196
131
|
}
|
|
197
132
|
}
|
|
133
|
+
async webSocketClose(_ws) {
|
|
134
|
+
// context(justinvdm, 29 Jun 2026): Subscriptions are persisted on the
|
|
135
|
+
// WebSocket attachment. When the runtime removes the socket, the
|
|
136
|
+
// attachment disappears with it, so no persistent cleanup is required.
|
|
137
|
+
}
|
|
198
138
|
// Public RPC surface exposed to handler callbacks and other Workers RPC callers.
|
|
199
139
|
async getState(key) {
|
|
200
140
|
return __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getState).call(this, key, undefined);
|
|
@@ -203,7 +143,52 @@ export class SyncedStateServer extends DurableObject {
|
|
|
203
143
|
await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_setState).call(this, key, value, undefined);
|
|
204
144
|
}
|
|
205
145
|
}
|
|
206
|
-
_a = SyncedStateServer, _SyncedStateServer_stub = new WeakMap(), _SyncedStateServer_stateStore = new WeakMap(), _SyncedStateServer_stateStoreLoaded = new WeakMap(),
|
|
146
|
+
_a = SyncedStateServer, _SyncedStateServer_stub = new WeakMap(), _SyncedStateServer_stateStore = new WeakMap(), _SyncedStateServer_stateStoreLoaded = new WeakMap(), _SyncedStateServer_instances = new WeakSet(), _SyncedStateServer_handleClientMessage = async function _SyncedStateServer_handleClientMessage(ws, message) {
|
|
147
|
+
const identity = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getIdentity).call(this, ws);
|
|
148
|
+
const storageKey = await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_resolveStorageKey).call(this, message.key, identity);
|
|
149
|
+
switch (message.kind) {
|
|
150
|
+
case "getState": {
|
|
151
|
+
const value = await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getState).call(this, storageKey, identity);
|
|
152
|
+
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
|
|
153
|
+
kind: "getState",
|
|
154
|
+
key: message.key,
|
|
155
|
+
value,
|
|
156
|
+
id: message.id,
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case "setState": {
|
|
161
|
+
await __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_setState).call(this, storageKey, message.value, identity);
|
|
162
|
+
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
|
|
163
|
+
kind: "setState",
|
|
164
|
+
key: message.key,
|
|
165
|
+
id: message.id,
|
|
166
|
+
});
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case "subscribe": {
|
|
170
|
+
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_subscribe).call(this, ws, storageKey, message.key);
|
|
171
|
+
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
|
|
172
|
+
kind: "subscribe",
|
|
173
|
+
key: message.key,
|
|
174
|
+
id: message.id,
|
|
175
|
+
});
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case "unsubscribe": {
|
|
179
|
+
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_unsubscribe).call(this, ws, storageKey, message.key);
|
|
180
|
+
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_send).call(this, ws, {
|
|
181
|
+
kind: "unsubscribe",
|
|
182
|
+
key: message.key,
|
|
183
|
+
id: message.id,
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
default: {
|
|
188
|
+
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_sendError).call(this, ws, "Unknown message kind", message.id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}, _SyncedStateServer_loadStateStore = async function _SyncedStateServer_loadStateStore() {
|
|
207
192
|
if (__classPrivateFieldGet(this, _SyncedStateServer_stateStoreLoaded, "f")) {
|
|
208
193
|
return;
|
|
209
194
|
}
|
|
@@ -239,20 +224,14 @@ _a = SyncedStateServer, _SyncedStateServer_stub = new WeakMap(), _SyncedStateSer
|
|
|
239
224
|
}
|
|
240
225
|
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_broadcastUpdate).call(this, key, value);
|
|
241
226
|
}, _SyncedStateServer_subscribe = function _SyncedStateServer_subscribe(ws, storageKey, userKey) {
|
|
242
|
-
if (!__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").has(storageKey)) {
|
|
243
|
-
__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").set(storageKey, new Set());
|
|
244
|
-
}
|
|
245
|
-
const subscribers = __classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").get(storageKey);
|
|
246
227
|
// Defensive deduplication: a stateful client may send subscribe more than
|
|
247
|
-
// once for the same key (e.g. across reconnects)
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
228
|
+
// once for the same key (e.g. across reconnects). Keep only one entry per
|
|
229
|
+
// (socket, userKey) pair in the attachment.
|
|
230
|
+
const subs = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getSubscriptionsFromAttachment).call(this, ws);
|
|
231
|
+
if (!subs.some((s) => s.userKey === userKey && s.storageKey === storageKey)) {
|
|
232
|
+
subs.push({ userKey, storageKey });
|
|
233
|
+
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_setSubscriptionsInAttachment).call(this, ws, subs);
|
|
254
234
|
}
|
|
255
|
-
subscribers.add({ ws, userKey });
|
|
256
235
|
const identity = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getIdentity).call(this, ws);
|
|
257
236
|
const subscribeHandler = __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_subscribeHandler);
|
|
258
237
|
if (subscribeHandler) {
|
|
@@ -261,26 +240,9 @@ _a = SyncedStateServer, _SyncedStateServer_stub = new WeakMap(), _SyncedStateSer
|
|
|
261
240
|
subscribeHandler(storageKey, identity, stub);
|
|
262
241
|
}
|
|
263
242
|
}
|
|
264
|
-
// Persist the subscription in the socket attachment so it survives
|
|
265
|
-
// DO eviction.
|
|
266
|
-
const subs = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getSubscriptionsFromAttachment).call(this, ws);
|
|
267
|
-
if (!subs.some((s) => s.userKey === userKey && s.storageKey === storageKey)) {
|
|
268
|
-
subs.push({ userKey, storageKey });
|
|
269
|
-
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_setSubscriptionsInAttachment).call(this, ws, subs);
|
|
270
|
-
}
|
|
271
243
|
}, _SyncedStateServer_unsubscribe = function _SyncedStateServer_unsubscribe(ws, storageKey, userKey) {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
for (const entry of subscribers) {
|
|
275
|
-
if (entry.ws === ws && entry.userKey === userKey) {
|
|
276
|
-
subscribers.delete(entry);
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
if (subscribers.size === 0) {
|
|
281
|
-
__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").delete(storageKey);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
244
|
+
const subs = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getSubscriptionsFromAttachment).call(this, ws).filter((s) => !(s.userKey === userKey && s.storageKey === storageKey));
|
|
245
|
+
__classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_setSubscriptionsInAttachment).call(this, ws, subs);
|
|
284
246
|
const identity = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getIdentity).call(this, ws);
|
|
285
247
|
const unsubscribeHandler = __classPrivateFieldGet(_a, _a, "f", _SyncedStateServer_unsubscribeHandler);
|
|
286
248
|
if (unsubscribeHandler) {
|
|
@@ -289,24 +251,27 @@ _a = SyncedStateServer, _SyncedStateServer_stub = new WeakMap(), _SyncedStateSer
|
|
|
289
251
|
unsubscribeHandler(storageKey, identity, stub);
|
|
290
252
|
}
|
|
291
253
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
254
|
+
}, _SyncedStateServer_broadcastUpdate = function _SyncedStateServer_broadcastUpdate(storageKey, value) {
|
|
255
|
+
// Cloudflare's live socket list is the source of truth for currently
|
|
256
|
+
// connected sockets. Each socket's attachment holds the user-facing keys it
|
|
257
|
+
// subscribed to, so we send each socket its original key even after the DO
|
|
258
|
+
// has hibernated.
|
|
259
|
+
for (const ws of this.ctx.getWebSockets()) {
|
|
260
|
+
const subs = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getSubscriptionsFromAttachment).call(this, ws);
|
|
261
|
+
for (const sub of subs) {
|
|
262
|
+
if (sub.storageKey !== storageKey)
|
|
263
|
+
continue;
|
|
264
|
+
const message = {
|
|
265
|
+
kind: "update",
|
|
266
|
+
key: sub.userKey,
|
|
267
|
+
value,
|
|
268
|
+
};
|
|
269
|
+
try {
|
|
270
|
+
ws.send(packMessage(message));
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Socket is already closed; it will be cleaned up via webSocketClose.
|
|
274
|
+
}
|
|
310
275
|
}
|
|
311
276
|
}
|
|
312
277
|
}, _SyncedStateServer_getAttachment = function _SyncedStateServer_getAttachment(ws) {
|
|
@@ -326,24 +291,6 @@ _a = SyncedStateServer, _SyncedStateServer_stub = new WeakMap(), _SyncedStateSer
|
|
|
326
291
|
const attachment = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getAttachment).call(this, ws);
|
|
327
292
|
attachment.subscriptions = subscriptions;
|
|
328
293
|
ws.serializeAttachment(attachment);
|
|
329
|
-
}, _SyncedStateServer_ensureSubscriptionsLoaded = function _SyncedStateServer_ensureSubscriptionsLoaded(ws) {
|
|
330
|
-
const subs = __classPrivateFieldGet(this, _SyncedStateServer_instances, "m", _SyncedStateServer_getSubscriptionsFromAttachment).call(this, ws);
|
|
331
|
-
for (const { userKey, storageKey } of subs) {
|
|
332
|
-
if (!__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").has(storageKey)) {
|
|
333
|
-
__classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").set(storageKey, new Set());
|
|
334
|
-
}
|
|
335
|
-
const subscribers = __classPrivateFieldGet(this, _SyncedStateServer_subscriptions, "f").get(storageKey);
|
|
336
|
-
let exists = false;
|
|
337
|
-
for (const entry of subscribers) {
|
|
338
|
-
if (entry.ws === ws && entry.userKey === userKey) {
|
|
339
|
-
exists = true;
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
if (!exists) {
|
|
344
|
-
subscribers.add({ ws, userKey });
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
294
|
}, _SyncedStateServer_resolveStorageKey =
|
|
348
295
|
// ---------------------------------------------------------------------------
|
|
349
296
|
// Handlers
|
|
@@ -30,12 +30,22 @@ export function createSyncedStateClient(endpoint, webSocketFactory) {
|
|
|
30
30
|
}
|
|
31
31
|
handlers.add(handler);
|
|
32
32
|
if (connection.isOpen) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
try {
|
|
34
|
+
await sendMessage(connection, {
|
|
35
|
+
kind: "subscribe",
|
|
36
|
+
key,
|
|
37
|
+
id: makeMessageId(connection),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// Roll back local subscription state so we don't pretend to be
|
|
42
|
+
// subscribed when the server rejected or never processed it.
|
|
43
|
+
handlers.delete(handler);
|
|
44
|
+
if (handlers.size === 0)
|
|
45
|
+
connection.messageHandlers.delete(key);
|
|
46
|
+
manager.removeSubscription(key, handler, client);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
39
49
|
}
|
|
40
50
|
},
|
|
41
51
|
async unsubscribe(key, handler) {
|
|
@@ -49,13 +59,18 @@ export function createSyncedStateClient(endpoint, webSocketFactory) {
|
|
|
49
59
|
if (handlers.size === 0)
|
|
50
60
|
connection.messageHandlers.delete(key);
|
|
51
61
|
}
|
|
52
|
-
|
|
53
|
-
connection.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
try {
|
|
63
|
+
if (connection.isOpen) {
|
|
64
|
+
await sendMessage(connection, {
|
|
65
|
+
kind: "unsubscribe",
|
|
66
|
+
key,
|
|
67
|
+
id: makeMessageId(connection),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Unsubscribe is often called during cleanup. Swallow errors from a
|
|
73
|
+
// closing socket; the server attachment will be dropped anyway.
|
|
59
74
|
}
|
|
60
75
|
},
|
|
61
76
|
};
|
|
@@ -53,7 +53,9 @@ export class SyncedStateClientManager {
|
|
|
53
53
|
normalizeEndpoint(endpoint) {
|
|
54
54
|
if (endpoint.startsWith("/") && typeof window !== "undefined") {
|
|
55
55
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
56
|
-
|
|
56
|
+
// Strip a trailing DNS root dot so the WebSocket URL matches routing.
|
|
57
|
+
const host = window.location.host.replace(/\.$/, "");
|
|
58
|
+
return `${protocol}//${host}${endpoint}`;
|
|
57
59
|
}
|
|
58
60
|
return endpoint;
|
|
59
61
|
}
|
|
@@ -125,9 +127,9 @@ export class SyncedStateClientManager {
|
|
|
125
127
|
const normalized = this.normalizeEndpoint(endpoint);
|
|
126
128
|
const connection = this.getConnection(normalized);
|
|
127
129
|
if (connection) {
|
|
128
|
-
if (connection.
|
|
129
|
-
clearTimeout(connection.
|
|
130
|
-
connection.
|
|
130
|
+
if (connection.pendingRequestTimer) {
|
|
131
|
+
clearTimeout(connection.pendingRequestTimer);
|
|
132
|
+
connection.pendingRequestTimer = null;
|
|
131
133
|
}
|
|
132
134
|
try {
|
|
133
135
|
connection.ws.close();
|
|
@@ -23,21 +23,39 @@ export const syncedStateRoutes = (getNamespace, options = {}) => {
|
|
|
23
23
|
const roomHandler = SyncedStateServer.getRoomHandler();
|
|
24
24
|
const idParam = requestInfo.params?.id;
|
|
25
25
|
let resolvedRoomName;
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
try {
|
|
27
|
+
if (roomHandler) {
|
|
28
|
+
resolvedRoomName = await runWithRequestInfo(requestInfo, async () => await roomHandler(idParam, requestInfo));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
resolvedRoomName = idParam ?? durableObjectName;
|
|
32
|
+
}
|
|
28
33
|
}
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
catch (error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : "Room resolution failed";
|
|
36
|
+
return new Response(message, { status: 500 });
|
|
31
37
|
}
|
|
32
38
|
const id = namespace.idFromName(resolvedRoomName);
|
|
33
39
|
const stub = namespace.get(id);
|
|
34
40
|
let identity = undefined;
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
try {
|
|
42
|
+
if (identityExtractor) {
|
|
43
|
+
identity = await runWithRequestInfo(requestInfo, async () => await identityExtractor(requestInfo));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : "Identity extraction failed";
|
|
48
|
+
return new Response(message, { status: 500 });
|
|
37
49
|
}
|
|
38
50
|
const doUrl = new URL(request.url);
|
|
39
51
|
doUrl.searchParams.set("clientId", crypto.randomUUID());
|
|
40
|
-
|
|
52
|
+
try {
|
|
53
|
+
setIdentityInUrl(identity, doUrl);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const message = error instanceof Error ? error.message : "Invalid identity";
|
|
57
|
+
return new Response(message, { status: 500 });
|
|
58
|
+
}
|
|
41
59
|
const doRequest = new Request(doUrl.toString(), {
|
|
42
60
|
headers: request.headers,
|
|
43
61
|
});
|
|
@@ -12,7 +12,7 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
|
|
|
12
12
|
// context(justinvdm, 2026-05-06): Only set a sourcemap default if the user
|
|
13
13
|
// hasn't already configured it in their vite config. This lets users opt in
|
|
14
14
|
// or out explicitly while still providing a sensible mode-aware default.
|
|
15
|
-
const sourcemap = config.build?.sourcemap ??
|
|
15
|
+
const sourcemap = config.build?.sourcemap ?? mode === "development";
|
|
16
16
|
const workerConfig = {
|
|
17
17
|
resolve: {
|
|
18
18
|
conditions: [
|
|
@@ -47,6 +47,8 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
|
|
|
47
47
|
"rwsdk/realtime/worker",
|
|
48
48
|
"rwsdk/router",
|
|
49
49
|
"rwsdk/worker",
|
|
50
|
+
"rwsdk/use-synced-state/worker",
|
|
51
|
+
"rwsdk/use-synced-state/hibernation/worker",
|
|
50
52
|
],
|
|
51
53
|
exclude: [],
|
|
52
54
|
entries: [workerEntryPathname],
|
|
@@ -55,7 +57,7 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
|
|
|
55
57
|
jsx: "react-jsx",
|
|
56
58
|
define: {
|
|
57
59
|
"process.env.NODE_ENV": JSON.stringify(mode),
|
|
58
|
-
|
|
60
|
+
__webpack_require__: "globalThis.__webpack_require__",
|
|
59
61
|
},
|
|
60
62
|
},
|
|
61
63
|
},
|
|
@@ -108,6 +110,7 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
|
|
|
108
110
|
"rwsdk/router",
|
|
109
111
|
"rwsdk/turnstile",
|
|
110
112
|
"rwsdk/use-synced-state/client",
|
|
113
|
+
"rwsdk/use-synced-state/hibernation/client",
|
|
111
114
|
],
|
|
112
115
|
entries: [],
|
|
113
116
|
rolldownOptions: {
|
|
@@ -115,7 +118,7 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
|
|
|
115
118
|
jsx: "react-jsx",
|
|
116
119
|
define: {
|
|
117
120
|
"process.env.NODE_ENV": JSON.stringify(mode),
|
|
118
|
-
|
|
121
|
+
__webpack_require__: "globalThis.__webpack_require__",
|
|
119
122
|
},
|
|
120
123
|
},
|
|
121
124
|
},
|
|
@@ -148,13 +151,14 @@ export const configPlugin = ({ silent, projectRootDir, workerEntryPathname, clie
|
|
|
148
151
|
"rwsdk/realtime/durableObject",
|
|
149
152
|
"rwsdk/realtime/worker",
|
|
150
153
|
"rwsdk/use-synced-state/client",
|
|
154
|
+
"rwsdk/use-synced-state/hibernation/client",
|
|
151
155
|
],
|
|
152
156
|
rolldownOptions: {
|
|
153
157
|
transform: {
|
|
154
158
|
jsx: "react-jsx",
|
|
155
159
|
define: {
|
|
156
160
|
"process.env.NODE_ENV": JSON.stringify(mode),
|
|
157
|
-
|
|
161
|
+
__webpack_require__: "globalThis.__webpack_require__",
|
|
158
162
|
},
|
|
159
163
|
},
|
|
160
164
|
},
|