@signalwire/js 4.0.0-rc.0 → 4.0.0-rc.1

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/browser.mjs CHANGED
@@ -13367,7 +13367,9 @@ const DEFAULT_MEMBER_CAPABILITIES = {
13367
13367
  position: false,
13368
13368
  meta: false,
13369
13369
  remove: false,
13370
- audioFlags: false
13370
+ audioFlags: false,
13371
+ denoise: false,
13372
+ lowbitrate: false
13371
13373
  };
13372
13374
  /**
13373
13375
  * Default call capabilities with no permissions
@@ -13440,7 +13442,9 @@ function computeMemberCapabilities(flags, memberType) {
13440
13442
  position: hasBooleanCapability(flags, memberType, null, "position"),
13441
13443
  meta: hasBooleanCapability(flags, memberType, null, "meta"),
13442
13444
  remove: hasBooleanCapability(flags, memberType, null, "remove"),
13443
- audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags")
13445
+ audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags"),
13446
+ denoise: hasBooleanCapability(flags, memberType, null, "denoise"),
13447
+ lowbitrate: hasBooleanCapability(flags, memberType, null, "lowbitrate")
13444
13448
  };
13445
13449
  }
13446
13450
  /**
@@ -13823,6 +13827,10 @@ var Participant = class extends Destroyable {
13823
13827
  get nodeId() {
13824
13828
  return this._state$.value.node_id;
13825
13829
  }
13830
+ /** Call ID for this participant's leg, or `undefined` if not available. */
13831
+ get callId() {
13832
+ return this._state$.value.call_id;
13833
+ }
13826
13834
  /** @internal */
13827
13835
  get value() {
13828
13836
  return this._state$.value;
@@ -13884,8 +13892,9 @@ var Participant = class extends Destroyable {
13884
13892
  noise_suppression: !this.noiseSuppression
13885
13893
  });
13886
13894
  }
13895
+ /** Toggles low-bitrate mode for this participant's media. */
13887
13896
  async toggleLowbitrate() {
13888
- throw new UnimplementedError();
13897
+ await this.executeMethod(this.id, "call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13889
13898
  }
13890
13899
  /**
13891
13900
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -13932,10 +13941,27 @@ var Participant = class extends Destroyable {
13932
13941
  }
13933
13942
  /**
13934
13943
  * Sets the participant's position in the video layout.
13944
+ *
13945
+ * Requires the `member.position` capability. The gateway keys positions by the
13946
+ * **target member's own** `call_id`/`node_id` (see issue #19400 and the legacy
13947
+ * `setPositions` implementation), so this sends the participant's own call
13948
+ * context — matching {@link Participant.remove}. A resolved promise does not
13949
+ * guarantee a visible change: the backend silently returns `200` (no-op) for
13950
+ * non-conference targets.
13951
+ *
13935
13952
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
13936
13953
  */
13937
13954
  async setPosition(value) {
13938
- await this.executeMethod(this.id, "call.member.position.set", { position: value });
13955
+ const state = this._state$.value;
13956
+ const target = {
13957
+ member_id: this.id,
13958
+ call_id: state.call_id ?? "",
13959
+ node_id: state.node_id ?? ""
13960
+ };
13961
+ await this.executeMethod(target, "call.member.position.set", { targets: [{
13962
+ target,
13963
+ position: value
13964
+ }] });
13939
13965
  }
13940
13966
  /** Removes this participant from the call. */
13941
13967
  async remove() {
@@ -16481,7 +16507,11 @@ function isVertoInviteMessage(value) {
16481
16507
  const msg = value;
16482
16508
  return msg.method === "verto.invite" && isObject(msg.params) && hasProperty(msg.params, "sdp") && hasProperty(msg.params, "callID");
16483
16509
  }
16484
- function isVertoByeMessage(value) {
16510
+ /**
16511
+ * Guards inbound `verto.bye` frames (server → client). Outbound bye messages are
16512
+ * built locally and never type-guarded, so only the inbound shape is asserted.
16513
+ */
16514
+ function isVertoByeInboundMessage(value) {
16485
16515
  if (!isVertoMethodMessage(value)) return false;
16486
16516
  return value.method === "verto.bye";
16487
16517
  }
@@ -16501,6 +16531,14 @@ function isVertoMediaParamsInnerParams(value) {
16501
16531
  function isVertoPingInnerParams(value) {
16502
16532
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "method") && value.method === "verto.ping";
16503
16533
  }
16534
+ /**
16535
+ * Guards the params of an inbound `verto.bye` frame (top-level `callID`). Use
16536
+ * this to recognise the bye payload extracted by `vertoBye$`, e.g. when racing
16537
+ * it against a boolean answer/reject signal.
16538
+ */
16539
+ function isVertoByeInboundParamsGuard(value) {
16540
+ return isObject(value) && hasProperty(value, "callID") && typeof value.callID === "string";
16541
+ }
16504
16542
 
16505
16543
  //#endregion
16506
16544
  //#region src/managers/VertoManager.ts
@@ -16839,7 +16877,7 @@ var WebRTCVertoManager = class extends VertoManager {
16839
16877
  return this.cachedObservable("vertoMediaParams$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaParamsInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16840
16878
  }
16841
16879
  get vertoBye$() {
16842
- return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16880
+ return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeInboundMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16843
16881
  }
16844
16882
  get vertoAttach$() {
16845
16883
  return this.cachedObservable("vertoAttach$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoAttachMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
@@ -16992,7 +17030,7 @@ var WebRTCVertoManager = class extends VertoManager {
16992
17030
  logger$16.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
16993
17031
  return;
16994
17032
  }
16995
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17033
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
16996
17034
  logger$16.info("[WebRTCManager] Inbound call ended by remote before answer.");
16997
17035
  this.callSession?.destroy();
16998
17036
  } else if (!vertoByeOrAccepted) {
@@ -17094,7 +17132,7 @@ var WebRTCVertoManager = class extends VertoManager {
17094
17132
  logger$16.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17095
17133
  return;
17096
17134
  }
17097
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17135
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17098
17136
  logger$16.info("[WebRTCManager] Call ended before answer was sent.");
17099
17137
  this.callSession?.destroy();
17100
17138
  } else if (!vertoByeOrAccepted) {
@@ -18147,6 +18185,12 @@ function mosToQualityLevel(mos) {
18147
18185
  var import_cjs$11 = require_cjs();
18148
18186
  const logger$12 = getLogger();
18149
18187
  /**
18188
+ * Verto method for setting member layout positions. Its gateway DTO requires a
18189
+ * `targets` array whose entries are `{ target, position }` (NOT bare targets),
18190
+ * so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
18191
+ */
18192
+ const POSITION_SET_METHOD = "call.member.position.set";
18193
+ /**
18150
18194
  * Ratio between the critical and warning RTT spike multipliers.
18151
18195
  * Warning threshold = baseline * warningMultiplier (default 3x)
18152
18196
  * Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
@@ -18353,7 +18397,7 @@ var WebRTCCall = class extends Destroyable {
18353
18397
  * @throws {JSONRPCError} If the RPC call returns an error.
18354
18398
  */
18355
18399
  async executeMethod(target, method, args) {
18356
- const params = this.buildMethodParams(target, args);
18400
+ const params = this.buildMethodParams(target, args, method);
18357
18401
  const request = buildRPCRequest({
18358
18402
  method,
18359
18403
  params
@@ -18367,12 +18411,16 @@ var WebRTCCall = class extends Destroyable {
18367
18411
  throw error;
18368
18412
  }
18369
18413
  }
18370
- buildMethodParams(target, args) {
18414
+ buildMethodParams(target, args, method) {
18371
18415
  const self = {
18372
18416
  node_id: this.nodeId ?? "",
18373
18417
  call_id: this.id,
18374
18418
  member_id: this.vertoManager.selfId ?? ""
18375
18419
  };
18420
+ if (method === POSITION_SET_METHOD) return {
18421
+ ...args,
18422
+ self
18423
+ };
18376
18424
  if (typeof target === "object") return {
18377
18425
  ...args,
18378
18426
  self,
@@ -18938,10 +18986,21 @@ var WebRTCCall = class extends Destroyable {
18938
18986
  return this.deferEmission(this._answered$.asObservable());
18939
18987
  }
18940
18988
  /**
18941
- * Sets the call layout and participant positions.
18989
+ * Sets the call layout and, optionally, individual participant positions.
18990
+ *
18991
+ * The gateway `call.layout.set` DTO has **no** `positions` member, so when
18992
+ * `positions` is provided this method issues a `call.member.position.set`
18993
+ * request per member (via {@link Participant.setPosition}, which keys each
18994
+ * position by that member's own call context) alongside `call.layout.set`
18995
+ * (issue #19400, Flag #6).
18996
+ *
18997
+ * **These operations are NOT atomic.** The layout is applied first, then each
18998
+ * member position sequentially, so members may briefly flash into their
18999
+ * default slots before being moved to the requested positions.
18942
19000
  *
18943
19001
  * @param layout - Layout name (must be one of {@link layouts}).
18944
- * @param positions - Map of member IDs to {@link VideoPosition} values.
19002
+ * @param positions - Optional map of member IDs to {@link VideoPosition} values.
19003
+ * When omitted or empty, only the layout is changed.
18945
19004
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
18946
19005
  *
18947
19006
  * @example
@@ -18954,10 +19013,17 @@ var WebRTCCall = class extends Destroyable {
18954
19013
  async setLayout(layout, positions) {
18955
19014
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
18956
19015
  const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
18957
- await this.executeMethod(selfId, "call.layout.set", {
18958
- layout,
18959
- positions
18960
- });
19016
+ await this.executeMethod(selfId, "call.layout.set", { layout });
19017
+ const positionEntries = Object.entries(positions ?? {});
19018
+ if (positionEntries.length === 0) return;
19019
+ for (const [memberId, position] of positionEntries) {
19020
+ const participant = this.participants.find((p) => p.id === memberId);
19021
+ if (!participant) {
19022
+ logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
19023
+ continue;
19024
+ }
19025
+ await participant.setPosition(position);
19026
+ }
18961
19027
  }
18962
19028
  /**
18963
19029
  * Transfers the call to another destination.
@@ -19876,11 +19942,41 @@ var ClientSessionManager = class extends Destroyable {
19876
19942
  }
19877
19943
  } else this._errors$.next(error);
19878
19944
  }
19945
+ /**
19946
+ * Clear the resume state (authorization_state + protocol) only.
19947
+ *
19948
+ * This is the stale-auth-state recovery helper used by handleAuthError:
19949
+ * the server rejected a reconnect, so the resume state is discarded and a
19950
+ * fresh connect follows. Attach records are deliberately preserved — the
19951
+ * session lives on through the reconnect and reattachCalls() needs the
19952
+ * stored call references afterwards. Do NOT add detachAll() here.
19953
+ *
19954
+ * For public teardown (disconnect/destroy), use {@link teardownSessionState}
19955
+ * instead, which clears the attach records as well.
19956
+ */
19879
19957
  async cleanupStoredConnectionParams() {
19880
19958
  await this.transport.setProtocol(void 0);
19881
19959
  await this.updateAuthorizationStateInStorage(void 0);
19882
19960
  this._authorization$.next(void 0);
19883
19961
  }
19962
+ /**
19963
+ * Public-teardown helper for disconnect()/destroy(). Clears the resume
19964
+ * state (authorization_state + protocol) AND the attach records as one
19965
+ * atomic unit.
19966
+ *
19967
+ * The two stores are coupled: the backend only honors attach records
19968
+ * within the session identified by the resume state, so ending the
19969
+ * session must clear both. Clearing one without the other strands records
19970
+ * no future session can honor (disconnect) or revives a session the
19971
+ * developer explicitly ended (destroy).
19972
+ *
19973
+ * Distinct from {@link cleanupStoredConnectionParams}, which keeps the
19974
+ * attach records for the stale-auth-state recovery path.
19975
+ */
19976
+ async teardownSessionState() {
19977
+ await this.cleanupStoredConnectionParams();
19978
+ await this.attachManager.detachAll();
19979
+ }
19884
19980
  async updateAuthState(authorization_state) {
19885
19981
  try {
19886
19982
  await this.storage.setItem(this.authorizationStateKey, authorization_state);
@@ -19975,7 +20071,7 @@ var ClientSessionManager = class extends Destroyable {
19975
20071
  async disconnect() {
19976
20072
  this.transport.disconnect();
19977
20073
  this._authState$.next({ kind: "unauthenticated" });
19978
- await this.cleanupStoredConnectionParams();
20074
+ await this.teardownSessionState();
19979
20075
  }
19980
20076
  async createInboundCall(invite) {
19981
20077
  const callSession = await this.createCall({
@@ -21142,6 +21238,10 @@ var TransportManager = class extends Destroyable {
21142
21238
  if (!isSignalwireRequest(message)) return true;
21143
21239
  const eventChannel = message.params.event_channel;
21144
21240
  if (!eventChannel) return true;
21241
+ if (message.params.event_type.startsWith("conversation.")) {
21242
+ logger$2.debug(`[Transport] Received conversation event: ${message.params.event_type} (event_channel: ${eventChannel})`);
21243
+ return true;
21244
+ }
21145
21245
  const currentProtocol = this._currentProtocol;
21146
21246
  if (!currentProtocol) return true;
21147
21247
  if (!eventChannel.includes(currentProtocol)) {
@@ -21742,7 +21842,7 @@ var SignalWire = class extends Destroyable {
21742
21842
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
21743
21843
  }
21744
21844
  try {
21745
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.0" });
21845
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.1" });
21746
21846
  } catch (error) {
21747
21847
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
21748
21848
  }
@@ -21750,6 +21850,14 @@ var SignalWire = class extends Destroyable {
21750
21850
  /**
21751
21851
  * Disconnects the WebSocket and tears down the current session.
21752
21852
  *
21853
+ * Ends the session identified by the protocol and clears its persisted
21854
+ * resume state (`authorization_state` + protocol) and attach records
21855
+ * together — a later {@link connect} with the same credentials starts a
21856
+ * fresh session and cannot reattach to the ended session's calls.
21857
+ * Credentials and device preferences are preserved. To temporarily stop
21858
+ * receiving inbound calls while keeping the session alive, use
21859
+ * `unregister()` instead.
21860
+ *
21753
21861
  * The client can be reconnected by calling {@link connect} again,
21754
21862
  * which creates a fresh transport and session.
21755
21863
  */
@@ -22141,12 +22249,20 @@ var SignalWire = class extends Destroyable {
22141
22249
  prefs.preferredVideoInput = null;
22142
22250
  await this._deviceController.clearDeviceState();
22143
22251
  }
22144
- /** Destroys the client, clearing timers and releasing all resources. */
22252
+ /**
22253
+ * Destroys the client, clearing timers and releasing all resources.
22254
+ *
22255
+ * Intentionally destroying the client ends its session: the resume state
22256
+ * (`authorization_state` + protocol) and the attach records are both
22257
+ * cleared. Credentials and device preferences are preserved — use
22258
+ * {@link resetToDefaults} for a full wipe. To temporarily stop receiving
22259
+ * inbound calls while keeping the session alive, use `unregister()`.
22260
+ */
22145
22261
  destroy() {
22146
22262
  this._refreshCoordinator?.destroy();
22147
22263
  this._refreshCoordinator = void 0;
22148
22264
  this._dpopManager?.destroy();
22149
- if (this._attachManager) this._attachManager.detachAll();
22265
+ this._clientSession.teardownSessionState();
22150
22266
  this._transport.destroy();
22151
22267
  this._clientSession.destroy();
22152
22268
  try {
@@ -22261,7 +22377,7 @@ var StaticCredentialProvider = class {
22261
22377
  /**
22262
22378
  * Library version from package.json, injected at build time.
22263
22379
  */
22264
- const version = "4.0.0-rc.0";
22380
+ const version = "4.0.0-rc.1";
22265
22381
  /**
22266
22382
  * Flag indicating the library has been loaded and is ready to use.
22267
22383
  * For UMD builds: `window.SignalWire.ready`
@@ -22283,7 +22399,7 @@ const ready = true;
22283
22399
  */
22284
22400
  const emitReadyEvent = () => {
22285
22401
  if (typeof window !== "undefined") {
22286
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.0" } });
22402
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.1" } });
22287
22403
  window.dispatchEvent(event);
22288
22404
  }
22289
22405
  };