Skip to content

feat(sockets): listener delivery strategies for throttling and batching - #147

Merged
prc5 merged 5 commits into
mainfrom
feat/sockets-listener-delivery
Oct 7, 2026
Merged

prc5 merged 5 commits into
mainfrom
feat/sockets-listener-delivery

Conversation

@prc5

@prc5 prc5 commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolves discussion #141: per-listener throttling for high-frequency WebSocket streams, configured on createListener so listen(), useListener and the SDK keep working unchanged.

const prices = socket.createListener<Price>()({
  topic: "prices",
  delivery: { strategy: "latest", interval: 16 },   // data: Price   (sampling)
});
const trades = socket.createListener<Trade>()({
  topic: "trades",
  delivery: { strategy: "batch", interval: 100, maxSize: 500 }, // data: Trade[] (nothing dropped)
});
const ticks = socket.createListener<Tick>()({
  topic: "ticks",
  delivery: createDeliveryStrategy<Tick, Summary>(({ deliver }) => ({ push, dispose })), // data: Summary
});
prices.setDelivery({ strategy: "batch", interval: 50 }); // clone, like setOptions

Design

  • Placement: wrapping happens in Listener.listen, the one point every adapter (WS, SSE, Firebase) passes through. Zero overhead when delivery is omitted. The socket.events raw broadcast stays unthrottled for devtools.
  • Semantics: latest = leading + trailing sampling; batch = window from first message, maxSize early flush; extra = last message's extra; unsubscribe disposes timers and drops pending (nothing fires after unlisten()); every listen() call owns its own controller.
  • Typing: fifth Delivered generic on Listener (defaults to Response), inferred from delivery. ExtractListenerResponseType keeps meaning the wire type; new ExtractListenerDeliveredType. Covariance of Response is preserved (type-tested), so ListenerInstance<{ response: ... }> constraints keep working.
  • React: useListener data/onEvent typed by the delivered type; deps keyed on topic + options + delivery key (replaces the never-assigned listener.params).
  • SDK: delivery in defaults, delivered field on ListenerModel, InjectSocket preserves it.

Also in this PR

  • Fix: listener.params was declared but never assigned.
  • Two pre-existing flaky React specs made deterministic (mock default delay race, fixed sleep).
  • New project skill fix-flaky-tests with a repeat-run script.
  • Docs: two new guides (sockets + React "Throttling & Batching"), Listener/useListener/SDK reference updates, stale endpoint → topic fixes.

Tests

  • sockets: 39 files / 384 tests (104 new: strategy units, listener integration, SSE, e2e, SDK, type tests)
  • react: 49 files / 444 tests (16 new useListener delivery tests incl. StrictMode, unmount, re-subscribe, render counts)
  • typecheck green in sockets, react, testing, adapter-firebase(-admin), plugin-devtools, hyper-flow; docs build green

🤖 Generated with Claude Code

prc5 and others added 5 commits October 7, 2026 21:22
…hing

Add a `delivery` option to listeners so high-frequency streams can be sampled
(`latest`), batched (`batch`) or shaped by a custom strategy before reaching
`listen` callbacks. Wrapping happens in `Listener.listen`, so every adapter,
`useListener` and the SDK pick it up without changes. No `delivery` means the
callback is registered as-is with zero overhead.

- New `delivery/` module: types, `latest`/`batch` presets, `createDeliveryStrategy`,
  `createDeliveryController`, `getDeliveryKey`
- Fifth `Delivered` generic on `Listener` (defaults to `Response`), inferred from
  the `delivery` option; `batch` delivers `Response[]`
- `setDelivery()` clone method; `delivery` carried through setParams/setOptions/clone
- `ExtractListenerDeliveredType`; `delivered` slot on ListenerInstance, ListenerModel
  and ExtendListener
- SDK: `delivery` in `SocketSdkDefaults`, `InjectSocket` preserves the delivered type
- Fix: `listener.params` was declared but never assigned

Closes discussion #141.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Type `data` and `onEvent` by the listener's delivered type (arrays for `batch`),
and re-subscribe on `topic`, `options` and the delivery key instead of the
never-assigned `listener.params`. Presets compare by value, custom strategies
by identity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- useSubmit queue: the mock resolved (default 20ms delay) before the second hook
  mounted, so "loading" was already gone; give the request an explicit delay
- useFetch revalidate: replace a fixed 50ms sleep with waitFor on the callback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- New guides: sockets "Throttling & Batching" and React counterpart
- Listener reference: Delivery Strategies feature section, Delivered Type
- useListener reference: Throttling & Batching section
- SDK reference: `delivery` default and `delivered` model field
- Fix stale `endpoint` keys in socket docs (listeners and emitters use `topic`)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@prc5
prc5 requested a review from GerasNyx as a code owner October 7, 2026 19:22
@prc5
prc5 merged commit 3be730c into main Oct 7, 2026
4 checks passed
@prc5
prc5 deleted the feat/sockets-listener-delivery branch October 7, 2026 19:30
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 9.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant