rwsdk 1.2.12 → 1.2.13
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.
|
@@ -279,6 +279,30 @@ describe("client-core reconnection", () => {
|
|
|
279
279
|
expect(sharedCallback).toHaveBeenCalledWith("disconnected");
|
|
280
280
|
unsubB();
|
|
281
281
|
});
|
|
282
|
+
it("duplicates active subscriptions on every reconnect", async () => {
|
|
283
|
+
const client = getSyncedStateClient(ENDPOINT);
|
|
284
|
+
const handler = vi.fn();
|
|
285
|
+
await client.subscribe("counter", handler);
|
|
286
|
+
expect(__testing.activeSubscriptions.size).toBe(1);
|
|
287
|
+
// First reconnect
|
|
288
|
+
mockClients[0].simulateBreak();
|
|
289
|
+
vi.runOnlyPendingTimers();
|
|
290
|
+
await __testing.warmUp(ENDPOINT);
|
|
291
|
+
await vi.runAllTimersAsync();
|
|
292
|
+
// Second reconnect
|
|
293
|
+
mockClients[1].simulateBreak();
|
|
294
|
+
vi.runOnlyPendingTimers();
|
|
295
|
+
await __testing.warmUp(ENDPOINT);
|
|
296
|
+
await vi.runAllTimersAsync();
|
|
297
|
+
// Third reconnect
|
|
298
|
+
mockClients[2].simulateBreak();
|
|
299
|
+
vi.runOnlyPendingTimers();
|
|
300
|
+
await __testing.warmUp(ENDPOINT);
|
|
301
|
+
await vi.runAllTimersAsync();
|
|
302
|
+
// Should remain exactly one subscription per logical subscription.
|
|
303
|
+
// Buggy implementation doubles the set on every reconnect.
|
|
304
|
+
expect(__testing.activeSubscriptions.size).toBe(1);
|
|
305
|
+
});
|
|
282
306
|
it("BUG: reconnect emits 'connected' and resets backoff even when subscribe() rejects", async () => {
|
|
283
307
|
const client = getSyncedStateClient(ENDPOINT);
|
|
284
308
|
const handler = vi.fn();
|
|
@@ -35,6 +35,9 @@ function notifyStatusChange(endpoint, status) {
|
|
|
35
35
|
* Returns an unsubscribe function.
|
|
36
36
|
*/
|
|
37
37
|
export const onStatusChange = (endpoint, callback) => {
|
|
38
|
+
// Normalize the endpoint so listeners registered with a relative URL
|
|
39
|
+
// (e.g. "/__synced-state") are notified using the same key as the
|
|
40
|
+
// cached client and the reconnect path.
|
|
38
41
|
const normalized = normalizeEndpoint(endpoint);
|
|
39
42
|
let listeners = statusListeners.get(normalized);
|
|
40
43
|
if (!listeners) {
|
|
@@ -164,25 +167,29 @@ export const getSyncedStateClient = (endpoint = DEFAULT_SYNCED_STATE_PATH) => {
|
|
|
164
167
|
get(_target, prop) {
|
|
165
168
|
if (prop === "subscribe") {
|
|
166
169
|
return async (key, handler) => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
activeSubscriptions.
|
|
170
|
+
// Idempotent subscription registration. Reconnect mutates the
|
|
171
|
+
// existing entry's client in place and then re-subscribes on the
|
|
172
|
+
// new transport; without this guard we create duplicate registry
|
|
173
|
+
// entries that double on every reconnect and leak component
|
|
174
|
+
// closures after unmount.
|
|
175
|
+
const exists = [...activeSubscriptions].some((s) => s.key === key && s.handler === handler && s.client === wrappedClient);
|
|
176
|
+
if (!exists) {
|
|
177
|
+
activeSubscriptions.add({ key, handler, client: wrappedClient });
|
|
178
|
+
}
|
|
173
179
|
const base = await getBaseClient();
|
|
174
180
|
return base[prop](key, handler);
|
|
175
181
|
};
|
|
176
182
|
}
|
|
177
183
|
if (prop === "unsubscribe") {
|
|
178
184
|
return async (key, handler) => {
|
|
179
|
-
//
|
|
180
|
-
for
|
|
185
|
+
// Remove every matching registry entry. The reconnect path can
|
|
186
|
+
// create duplicates for the same (key, handler) pair, so a single
|
|
187
|
+
// component unmount must clean up all of them.
|
|
188
|
+
for (const sub of [...activeSubscriptions]) {
|
|
181
189
|
if (sub.key === key &&
|
|
182
190
|
sub.handler === handler &&
|
|
183
191
|
sub.client === wrappedClient) {
|
|
184
192
|
activeSubscriptions.delete(sub);
|
|
185
|
-
break;
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
195
|
const base = await getBaseClient();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rwsdk",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.13",
|
|
4
4
|
"description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
"semver": "~7.7.4",
|
|
220
220
|
"tsx": "~4.21.0",
|
|
221
221
|
"typescript": "~6.0.3",
|
|
222
|
-
"vite": "~7.3.
|
|
222
|
+
"vite": "~7.3.5",
|
|
223
223
|
"vitest": "~4.1.5"
|
|
224
224
|
}
|
|
225
225
|
}
|