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
- const subscription = {
168
- key,
169
- handler,
170
- client: wrappedClient,
171
- };
172
- activeSubscriptions.add(subscription);
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
- // Find and remove the subscription
180
- for (const sub of activeSubscriptions) {
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.12",
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.2",
222
+ "vite": "~7.3.5",
223
223
  "vitest": "~4.1.5"
224
224
  }
225
225
  }