# Changelog

All notable changes to `@metered-ca/realtime` are documented here. Format
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.2.0] — 2026-07-03

Data-channel-first release: a reliable re-create signal for
peer-to-peer data channels, plus stricter option validation.

### Added

- **`connection-reset` event on `RemotePeer`.** Fires when the SDK
  re-establishes the underlying connection to a peer after a
  transient signalling drop — a reliable signal to re-create anything
  you opened on the previous connection, above all data channels via
  `remote.pc.createDataChannel(...)`. By the time the handler runs,
  `remote.pc` already points at the fresh connection. It does not
  fire for the initial connection (use `peer-joined` there). For
  data-channel-only apps this is the signal to use: with no media
  tracks there is no negotiation activity to drive `state-change`
  back to `"connected"` until your app creates a channel on the new
  connection.
- **`OversizedPayloadError` from `SignallingClient.publish()` /
  `send()`.** Payloads whose JSON serialization exceeds the
  server-advertised `maxMessageSize` now reject client-side with a
  typed error (`size` / `cap` fields), keeping the connection — and
  every subscription on it — alive instead of the server closing the
  socket with code 1009. `MeteredPeer.send()` / `sendTo()` already
  had this behavior via `MeteredPeerOversizedError`; the raw client
  now matches its documented contract.

### Fixed

- **The data-channel example pattern could never connect in
  media-free apps.** The previous example (and docs guidance) created
  the channel only after the peer reached `"connected"` — but with no
  tracks, nothing starts the connection until a channel exists, so
  data-channel-only apps waited forever. The corrected pattern
  creates the channel at `peer-joined` (creating it is what starts
  the connection) and re-creates it on `connection-reset`. A
  browser-level test now exercises the full media-free flow,
  including recovery across a dropped connection.
- **`inactivityTimeoutMs` can now actually be disabled.** Setting it
  to `0` (or any non-positive / non-finite value) turns the liveness
  watchdog and its keepalive off together, matching the other Metered
  Realtime SDKs. Previously a value like `Infinity` had the opposite
  of the intended effect — connections closed immediately after
  opening instead of the watchdog being disabled. `NaN` now throws a
  `TypeError` at construction,
  and `tokenProviderTimeoutMs` is validated (finite, > 0) the same
  way the reconnect options already were.
- **A connection drop while a `MeteredPeer` sat idle after a failed
  `join()` no longer leaves a background connection reconnecting
  behind it.** The wrapper reports `closed` and the underlying client
  now stops with it; previously it kept reconnecting (indefinitely
  once the network recovered), holding a connection slot for a
  session the app had already given up on.
- `DataChannel.send()` docs corrected: queued sends settle
  independently — one failed send never rejects the sends queued
  behind it (the implementation always behaved this way).
- `ReconcileTimeoutError`'s message now gives the correct recovery
  advice (construct a new `MeteredPeer`; `close()` is terminal, so
  "re-join" on the same instance was impossible).
- README: the global-`WebSocket` claim is now accurate — Node 22+
  out of the box; Node 18–21 with a `webSocketFactory`.

## [1.1.0] — 2026-06-12

Reconnect-robustness release: hardens the automatic-recovery paths
(network switch, dropped connections, service restarts). New error
names are additive — branch on `err.name`, never on message text.

### Fixed

- **`stream-removed` now fires in real browsers.** When the sender
  removes a stream (`removeStream()` / the final `removeTrack()`), the
  receiver's tracks go quiet without ending in modern browsers — the
  event now also rides the receiving stream's own `removetrack` signal,
  so `stream-removed` fires as documented. Previously it only triggered
  when tracks fully ended (connection teardown), so sender-side
  removals went unnoticed. Verified against real Chromium in the
  browser test suite.
- **WebRTC recovery no longer stalls when a recovery offer goes
  unanswered.** If the other side never answers (its connection died
  mid-recovery), the SDK times the attempt out and moves on, preserving
  the guarantee that a dead peer connection eventually surfaces the
  terminal `IceRestartExhaustedError`. Previously one lost answer
  silently froze recovery with no signal.
- **Back-to-back renegotiations are no longer dropped.** An offer
  arriving immediately behind an answer (e.g. the remote starts a
  screen-share right after connecting) was misread as a collision and
  ignored by one side; it is now accepted per the WebRTC
  perfect-negotiation rules.
- **`connect()` → `close()` → `connect()` no longer opens two
  connections** when a token fetch is in flight: the superseded attempt
  aborts (its `connect()` rejects) instead of racing the new one.
- **A WebSocket construction failure now follows the normal disconnect
  flow.** When the socket can't be constructed (e.g. blocked by a
  Content-Security-Policy), the client emits `disconnected` and applies
  the standard retry policy, instead of silently freezing higher-level
  wrappers in `reconnecting`.
- **Subscription bookkeeping hygiene.** An invalid channel name now
  rejects with a `TypeError` before being remembered (it used to be
  re-attempted, and re-fail, on every reconnect); a subscribe attempted
  while disconnected rejects without leaving a surprise subscription
  behind for the next reconnect.
- **`close()` on an idle `MeteredPeer` now closes the underlying
  connection.** `join()` can fail at the subscribe step with the
  connection already up; closing the wrapper previously left that
  connection running.
- **A peer alone in its channel reconnects instantly.** A solo peer
  could previously stay stuck in `reconnecting` after a network blip;
  it now returns to `joined` immediately.
- **`off(handler)` now removes `once(handler)` registrations**,
  matching Node's EventEmitter semantics.
- **Stalled connection attempts fail fast and clean.** If the service
  accepts the socket but the connection never becomes ready, `connect()`
  now rejects AND tears the socket down, landing the client in `closed`
  so a retry can proceed immediately. Previously the client could stay
  wedged in `connecting` (blocking retries for up to a minute), and a
  late server handshake could flip an already-failed `connect()` to
  `connected`. The same stall during automatic reconnection now moves on
  to the next attempt in seconds instead of waiting on the inactivity
  watchdog.
- **A network drop in the middle of `join()` no longer leaves a
  background connection behind.** `join()` rejects, the peer returns to
  `idle`, and the same instance can retry `join()` cleanly. Previously
  the underlying connection kept reconnecting — and kept the failed
  join's channel subscribed — beneath a peer that reported itself
  closed, and the peer's state could illegally move out of the terminal
  `closed` state.
- **`SignallingClient.close()` called from inside a `disconnected`
  handler now reliably stops reconnection.** Previously the client could
  schedule (and run) another reconnect attempt after an explicit
  `close()` issued in that window.
- **Data-only peers no longer show `reconnecting` forever.** Remote
  peers that never had an active WebRTC connection (presence/chat-style
  usage with no tracks or data channels) now stay `idle` across a
  network blip instead of being parked at `reconnecting` permanently;
  their first real connection attempt later surfaces a genuine
  `connecting` state.
- **The automatic re-subscribe after reconnect now retries.** If the
  re-subscribe is rejected or its confirmation is lost, the client
  retries it (bounded, per connection) instead of leaving the
  subscription silently dead until the next reconnect.

### Added

- **`NotConnectedError`.** `subscribe` / `publish` / `send` rejections
  caused by the client not being connected now carry this stable
  `err.name` — distinct from `DisconnectedError` (sent, but the
  connection dropped before confirmation).
- **Reconnect option validation.** Invalid `reconnect` knobs
  (`multiplier` below 1, negative or non-finite delays, `jitterRatio`
  outside 0–1, non-integer `maxAttempts`) now throw a `TypeError` at
  construction instead of producing a zero-delay reconnect loop.
  `maxAttempts: Infinity` remains supported.
- **`sendTo` warns on WebRTC-shaped payloads.** A payload matching the
  SDK's internal signalling shape (`type: "rtc-description"` /
  `"rtc-ice"`) is consumed by the recipient SDK instead of reaching its
  `data` event; `sendTo` now logs a warning for it, same policy as the
  existing reserved-key warning.
- **Recovery watchdog on `MeteredPeer`.** If the channel's presence
  can't be restored shortly after a reconnect, the SDK re-issues the
  subscription itself; if that still doesn't restore the channel, it
  emits an `error` event with `err.name === "ReconcileTimeoutError"` so
  your app can recover deliberately (close and re-join) instead of
  showing "reconnecting" indefinitely.
- **The terminal WebRTC give-up signal is now identifiable.** When a
  peer connection's automatic recovery budget is spent, the final
  `negotiation-error` carries `err.name === "IceRestartExhaustedError"`.
  Every other `negotiation-error` is transient — log it if you like,
  but only this name means "this peer connection will not recover on
  its own."

### Changed

- **`ServerErrorEvent.code` is now an open string union.** Error codes
  from a newer server flow through with their real code — a correlated
  request rejects immediately instead of timing out — while the
  `ErrorCode` union still documents (and autocompletes) the known
  codes. If you switch exhaustively over `code`, add a `default` branch
  when upgrading.
- **Less error noise during routine recovery.** `negotiation-error` is
  no longer emitted when an offer/answer merely could not be *delivered*
  because the messaging connection is itself reconnecting (normal
  during a Wi-Fi → cellular switch). Delivery failures are reported via
  your injected `logger` instead, and recovery proceeds automatically.
  Simultaneous recovery attempts from both ends of a peer connection
  (e.g. after a TURN server restart) are likewise absorbed without
  spurious errors.
- **`new MeteredPeer({ autoResubscribe: false })` now throws a
  `TypeError`.** The option never worked with `MeteredPeer` — its
  channel recovery depends on the automatic re-subscribe — and
  previously caused peers to hang in `reconnecting` after the first
  network blip. It remains fully supported on `SignallingClient`.

## [1.0.8] — 2026-06-05

### Changed

- **Package renamed to `@metered-ca/realtime`.** Previously published as
  `@metered-ca/peer`; renamed to match the Metered Realtime Messaging
  product name. Update your install to `npm install @metered-ca/realtime`
  and imports to `from "@metered-ca/realtime"`. The old `@metered-ca/peer`
  package is deprecated and now re-exports this one, so existing installs
  keep working — please migrate when convenient.

No behavior changes; same API, same wire protocol, same bundle. Only the
package name changed — the `MeteredPeer` class and every other export are
identical.

## [1.0.7] — 2026-06-03

### Changed

- **README now documents key setup.** Added a "Get a key" section
  covering how to create a publishable key in the dashboard and the
  need to enable `Send` for WebRTC — it's off by default for
  publishable keys, and without it a call connects but never
  negotiates video.

No behavior changes; same API, same wire protocol, same bundle.

## [1.0.6] — 2026-06-01

### Changed

- **`homepage` now points at the SDK getting-started guide.**
  The npm page's "Homepage" link goes directly to
  [metered.ca/docs/realtime-messaging/sdk-javascript/getting-started](https://www.metered.ca/docs/realtime-messaging/sdk-javascript/getting-started)
  instead of the marketing site, saving customers one click to the
  install + first-call walkthrough.

No behavior changes; same API, same wire protocol, same bundle.

## [1.0.5] — 2026-06-01

### Changed

- **Package metadata now points at the public source repository.**
  `repository` and `bugs` fields added to `package.json`; the npm
  page links to [github.com/metered-ca/peer](https://github.com/metered-ca/peer).
- **Description updated** from "Metered Signalling Server" to
  "Metered Realtime Messaging service" to match the product branding
  used across the README and hosted docs.

No behavior changes; same API, same wire protocol, same bundle.

## [1.0.4] — 2026-05-29

### Fixed

- **Outbound payload size is now measured without requiring a global
  `TextEncoder`.** On JavaScript runtimes that don't provide one (some
  React Native / Hermes versions), the size check behind `send` /
  `sendTo` and the DataChannel wrapper previously threw on first use; it
  now falls back to an internal UTF-8 byte counter. No public API change.

### Documentation

- Added a React Native guide and example — the SDK runs on React Native
  via `react-native-webrtc`, using the `rtcPeerConnectionFactory` option
  (or `registerGlobals()`).

## [1.0.3] — 2026-05-28

### Fixed

- **`replaceTrack(track, null)` while alone in a channel now correctly
  stops the track.** Silencing a track while no peers were connected
  previously left it tracked, so a peer joining afterward would still
  receive it. It is now dropped as expected and not sent to later
  joiners.
- **A failure re-establishing one peer during reconnect no longer
  stalls the others.** If re-attaching media to a single peer threw
  while recovering from a transient WebSocket drop, the client could
  stay stuck in `reconnecting`. Recovery now continues for the
  remaining peers and the client returns to `joined`.
- **`negotiation-error` is emitted once when ICE recovery is
  exhausted.** On a permanently broken link the terminal
  `negotiation-error` now fires a single time instead of repeating on
  every subsequent ICE failure; it re-arms if the connection later
  recovers and fails again.
- **`close()` during token resolution no longer leaks a connection.**
  Calling `close()` while a `tokenProvider` was still resolving could
  leave a live WebSocket that escaped teardown and could reconnect
  after you closed the client. `close()` now reliably cancels the
  in-flight connect.

## [1.0.2] — 2026-05-28

### Fixed

- **Idle WebSocket connections no longer disconnect and reconnect on
  a fixed cadence.** Connections with no inbound application traffic
  — typical during a steady WebRTC call where all media flows
  peer-to-peer — would previously trip the SDK's inactivity watchdog
  about every minute even when the underlying link was healthy, and
  each cycle briefly renegotiated any active peer connections.

  The SDK now keeps idle connections fed with a lightweight
  application-level keepalive. Healthy connections stay open;
  genuinely dead links are still detected and the existing reconnect
  behavior is unchanged.

  No public API change. The keepalive interval scales automatically
  with `SignallingClientOptions.inactivityTimeoutMs`.

## [1.0.1] — 2026-05-22

### Documentation

- Full documentation moved to
  [metered.ca/docs/realtime-messaging/sdk-javascript](https://www.metered.ca/docs/realtime-messaging/sdk-javascript).
  Single source of truth, hosted, navigable, version-stamped.
- `README.md` slimmed to a quick-start + links into the hosted docs.
- `API.md` and `MIGRATION.md` removed from the npm tarball — content
  lives in the hosted docs going forward (`api-reference/*.md` and
  `migration/*.md` pages).
- `LICENSE` and `CHANGELOG.md` continue to ship in the tarball
  alongside the build outputs.

No code changes. The shipped SDK is byte-identical to 1.0.0.

## [1.0.0] — 2026-05-22

Initial public release.

### Signalling layer (`SignallingClient`)

- WebSocket transport over the Metered signalling-server JSON wire
  protocol (welcome / subscribe / unsubscribe / publish / send / ack
  / error / message / direct / presence / going-away)
- Dual auth paths: `apiKey` (publishable `pk_live_` key, browser-safe)
  or `tokenProvider` (callback returning a JWT minted from your
  `sk_live_` secret server-side)
- Auto-reconnection with exponential backoff + jitter, configurable
  via `ReconnectOptions`; inactivity watchdog that closes-and-reconnects
  on a silent socket
- Per-connection subscribe replay on reconnect (`autoResubscribe`),
  defaulting to enabled so customers don't silently lose subscriptions
- Server-error / token-provider-error events for surfacing protocol-
  level and auth-pipeline failures outside the Promise-rejection path
- `WsCloseCode` value export for branching on the close code

### Peer-to-peer orchestrator (`MeteredPeer`)

- Channel-based peer discovery via the server's presence events. Join
  a channel → SDK builds an `RTCPeerConnection` per peer, manages
  perfect-negotiation, ICE trickle, and automatic ICE restart for TURN
  failover / network roam
- `peer.send(data)` broadcast + `peer.sendTo(peerId, data)` direct,
  both server-routed via the signalling channel
- `data` event with `kind: "broadcast" | "direct"` discriminator,
  channel-scoped to peers visible via presence, optional
  `senderMetadata` field (when joined with `includeSenderMetadata: true`)
- `addStream(stream, metadata?)` and `addTrack(track, stream?, metadata?)`
  for media — sugar + primitive pair. Streams + tracks are tracked
  internally; new peers and reconcile survivors get the same media
  attached automatically
- `replaceTrack(oldTrack, newTrack)` with surgical partial-failure
  recovery via `MeteredPeerReplaceTrackError.{succeeded, failed}`
- Reconcile-across-reconnect: `RemotePeer` identity is preserved
  through transient WebSocket drops. Surviving peers' underlying
  `RTCPeerConnection` is replaced transparently inside the same
  `RemotePeer`, so customer-held refs stay valid

### Per-peer (`RemotePeer`)

- Per-track and per-stream events on the receive side:
  - `track` — every inbound track, with optional `metadata`
  - `stream-added` / `stream-removed` — symmetric with the sender's
    `addStream` / `removeStream`. Re-fires correctly across reconcile
- `negotiation-error` / `ice-candidate-error` for SDP / ICE failures
  (SDP credentials scrubbed from error messages + stacks)
- `data-channel` for inbound DCs opened by the remote
- `remote.pc` escape hatch — the underlying `RTCPeerConnection` for
  `getStats()`, custom DataChannels, low-level event listeners
- `remote.send(data)` shorthand for `peer.sendTo(remote.id, data)`

### DataChannel wrapper

- `DataChannel` class wrapping `remote.pc.createDataChannel(...)`
  with backpressure-aware async `send()`, queue-depth cap, typed
  `DataChannelOverflowError`

### Multi-stream + per-track metadata

- `StreamMetadata` bag stamped at `addStream` / `addTrack` time,
  propagated via a reserved signalling channel to every connected
  peer (and every future newcomer / reconcile survivor). Receiver
  surfaces it on `track` event payload + `stream-added` event
- Synchronous size validation at send time (`MeteredPeerOversizedError`),
  bounded FIFO cache at receive time (512 entries per remote peer)
- Strict schema validation on the receive side rejects malformed
  control messages (non-object, null, array metadata)

### Typed errors

- `SignallingConnectError` — WS closed before welcome (auth failure,
  quota rejection). Carries `closeCode` + `closeReason`
- `MeteredPeerSendError` — send-time validation failures. Codes:
  `reserved_channel` / `not_joined` / `invalid_args` / `self_send`
- `MeteredPeerStateError` — state-gate violations on
  `join` / `addStream` / `addTrack` / `removeStream` / `removeTrack` /
  `replaceTrack`. Carries `code` / `method` / `currentState`
- `MeteredPeerOversizedError` — payload size exceeded `maxMessageSize`.
  Carries `size` + `cap`
- `MeteredPeerReplaceTrackError` — partial fanout failure. Carries
  `succeeded` + `failed[]` peer lists for surgical recovery
- `DataChannelOverflowError` — DC backpressure cap exceeded

### Fatal error event

- `MeteredPeer.error` fires on conditions the reconnect ladder won't
  recover: terminal WS close codes (4001 / 4002 / 4003 / 4012 / 4020),
  in-band fatal server-errors (channel_not_authorized, etc.), and
  token-provider exhaustion. `err.name` carries the symbolic code

### Build outputs

- ESM (`dist/index.mjs`), CJS (`dist/index.cjs`), UMD with browser
  global `MeteredPeer` (`dist/index.umd.js`), bundled types
  (`dist/index.d.ts`)
- `sideEffects: false` for tree-shaking
- Tested across browser (Playwright, headless Chromium) and Node 18+
  (Vitest + `@roamhq/wrtc`)

[1.2.0]: https://www.npmjs.com/package/@metered-ca/realtime/v/1.2.0
[1.1.0]: https://www.npmjs.com/package/@metered-ca/realtime/v/1.1.0
[1.0.8]: https://www.npmjs.com/package/@metered-ca/realtime/v/1.0.8
[1.0.7]: https://www.npmjs.com/package/@metered-ca/peer/v/1.0.7
[1.0.6]: https://www.npmjs.com/package/@metered-ca/peer/v/1.0.6
[1.0.5]: https://www.npmjs.com/package/@metered-ca/peer/v/1.0.5
[1.0.4]: https://www.npmjs.com/package/@metered-ca/peer/v/1.0.4
[1.0.3]: https://www.npmjs.com/package/@metered-ca/peer/v/1.0.3
[1.0.2]: https://www.npmjs.com/package/@metered-ca/peer/v/1.0.2
[1.0.1]: https://www.npmjs.com/package/@metered-ca/peer/v/1.0.1
[1.0.0]: https://www.npmjs.com/package/@metered-ca/peer/v/1.0.0
