@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.
@@ -13373,7 +13373,9 @@ const DEFAULT_MEMBER_CAPABILITIES = {
13373
13373
  position: false,
13374
13374
  meta: false,
13375
13375
  remove: false,
13376
- audioFlags: false
13376
+ audioFlags: false,
13377
+ denoise: false,
13378
+ lowbitrate: false
13377
13379
  };
13378
13380
  /**
13379
13381
  * Default call capabilities with no permissions
@@ -13446,7 +13448,9 @@ function computeMemberCapabilities(flags, memberType) {
13446
13448
  position: hasBooleanCapability(flags, memberType, null, "position"),
13447
13449
  meta: hasBooleanCapability(flags, memberType, null, "meta"),
13448
13450
  remove: hasBooleanCapability(flags, memberType, null, "remove"),
13449
- audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags")
13451
+ audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags"),
13452
+ denoise: hasBooleanCapability(flags, memberType, null, "denoise"),
13453
+ lowbitrate: hasBooleanCapability(flags, memberType, null, "lowbitrate")
13450
13454
  };
13451
13455
  }
13452
13456
  /**
@@ -13829,6 +13833,10 @@ var Participant = class extends Destroyable {
13829
13833
  get nodeId() {
13830
13834
  return this._state$.value.node_id;
13831
13835
  }
13836
+ /** Call ID for this participant's leg, or `undefined` if not available. */
13837
+ get callId() {
13838
+ return this._state$.value.call_id;
13839
+ }
13832
13840
  /** @internal */
13833
13841
  get value() {
13834
13842
  return this._state$.value;
@@ -13890,8 +13898,9 @@ var Participant = class extends Destroyable {
13890
13898
  noise_suppression: !this.noiseSuppression
13891
13899
  });
13892
13900
  }
13901
+ /** Toggles low-bitrate mode for this participant's media. */
13893
13902
  async toggleLowbitrate() {
13894
- throw new UnimplementedError();
13903
+ await this.executeMethod(this.id, "call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13895
13904
  }
13896
13905
  /**
13897
13906
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -13938,10 +13947,27 @@ var Participant = class extends Destroyable {
13938
13947
  }
13939
13948
  /**
13940
13949
  * Sets the participant's position in the video layout.
13950
+ *
13951
+ * Requires the `member.position` capability. The gateway keys positions by the
13952
+ * **target member's own** `call_id`/`node_id` (see issue #19400 and the legacy
13953
+ * `setPositions` implementation), so this sends the participant's own call
13954
+ * context — matching {@link Participant.remove}. A resolved promise does not
13955
+ * guarantee a visible change: the backend silently returns `200` (no-op) for
13956
+ * non-conference targets.
13957
+ *
13941
13958
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
13942
13959
  */
13943
13960
  async setPosition(value) {
13944
- await this.executeMethod(this.id, "call.member.position.set", { position: value });
13961
+ const state = this._state$.value;
13962
+ const target = {
13963
+ member_id: this.id,
13964
+ call_id: state.call_id ?? "",
13965
+ node_id: state.node_id ?? ""
13966
+ };
13967
+ await this.executeMethod(target, "call.member.position.set", { targets: [{
13968
+ target,
13969
+ position: value
13970
+ }] });
13945
13971
  }
13946
13972
  /** Removes this participant from the call. */
13947
13973
  async remove() {
@@ -16487,7 +16513,11 @@ function isVertoInviteMessage(value) {
16487
16513
  const msg = value;
16488
16514
  return msg.method === "verto.invite" && isObject(msg.params) && hasProperty(msg.params, "sdp") && hasProperty(msg.params, "callID");
16489
16515
  }
16490
- function isVertoByeMessage(value) {
16516
+ /**
16517
+ * Guards inbound `verto.bye` frames (server → client). Outbound bye messages are
16518
+ * built locally and never type-guarded, so only the inbound shape is asserted.
16519
+ */
16520
+ function isVertoByeInboundMessage(value) {
16491
16521
  if (!isVertoMethodMessage(value)) return false;
16492
16522
  return value.method === "verto.bye";
16493
16523
  }
@@ -16507,6 +16537,14 @@ function isVertoMediaParamsInnerParams(value) {
16507
16537
  function isVertoPingInnerParams(value) {
16508
16538
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "method") && value.method === "verto.ping";
16509
16539
  }
16540
+ /**
16541
+ * Guards the params of an inbound `verto.bye` frame (top-level `callID`). Use
16542
+ * this to recognise the bye payload extracted by `vertoBye$`, e.g. when racing
16543
+ * it against a boolean answer/reject signal.
16544
+ */
16545
+ function isVertoByeInboundParamsGuard(value) {
16546
+ return isObject(value) && hasProperty(value, "callID") && typeof value.callID === "string";
16547
+ }
16510
16548
 
16511
16549
  //#endregion
16512
16550
  //#region src/managers/VertoManager.ts
@@ -16845,7 +16883,7 @@ var WebRTCVertoManager = class extends VertoManager {
16845
16883
  return this.cachedObservable("vertoMediaParams$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaParamsInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16846
16884
  }
16847
16885
  get vertoBye$() {
16848
- return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16886
+ return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeInboundMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16849
16887
  }
16850
16888
  get vertoAttach$() {
16851
16889
  return this.cachedObservable("vertoAttach$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoAttachMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
@@ -16998,7 +17036,7 @@ var WebRTCVertoManager = class extends VertoManager {
16998
17036
  logger$16.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
16999
17037
  return;
17000
17038
  }
17001
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17039
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17002
17040
  logger$16.info("[WebRTCManager] Inbound call ended by remote before answer.");
17003
17041
  this.callSession?.destroy();
17004
17042
  } else if (!vertoByeOrAccepted) {
@@ -17100,7 +17138,7 @@ var WebRTCVertoManager = class extends VertoManager {
17100
17138
  logger$16.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17101
17139
  return;
17102
17140
  }
17103
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17141
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17104
17142
  logger$16.info("[WebRTCManager] Call ended before answer was sent.");
17105
17143
  this.callSession?.destroy();
17106
17144
  } else if (!vertoByeOrAccepted) {
@@ -18153,6 +18191,12 @@ function mosToQualityLevel(mos) {
18153
18191
  var import_cjs$11 = require_cjs();
18154
18192
  const logger$12 = getLogger();
18155
18193
  /**
18194
+ * Verto method for setting member layout positions. Its gateway DTO requires a
18195
+ * `targets` array whose entries are `{ target, position }` (NOT bare targets),
18196
+ * so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
18197
+ */
18198
+ const POSITION_SET_METHOD = "call.member.position.set";
18199
+ /**
18156
18200
  * Ratio between the critical and warning RTT spike multipliers.
18157
18201
  * Warning threshold = baseline * warningMultiplier (default 3x)
18158
18202
  * Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
@@ -18359,7 +18403,7 @@ var WebRTCCall = class extends Destroyable {
18359
18403
  * @throws {JSONRPCError} If the RPC call returns an error.
18360
18404
  */
18361
18405
  async executeMethod(target, method, args) {
18362
- const params = this.buildMethodParams(target, args);
18406
+ const params = this.buildMethodParams(target, args, method);
18363
18407
  const request = buildRPCRequest({
18364
18408
  method,
18365
18409
  params
@@ -18373,12 +18417,16 @@ var WebRTCCall = class extends Destroyable {
18373
18417
  throw error;
18374
18418
  }
18375
18419
  }
18376
- buildMethodParams(target, args) {
18420
+ buildMethodParams(target, args, method) {
18377
18421
  const self = {
18378
18422
  node_id: this.nodeId ?? "",
18379
18423
  call_id: this.id,
18380
18424
  member_id: this.vertoManager.selfId ?? ""
18381
18425
  };
18426
+ if (method === POSITION_SET_METHOD) return {
18427
+ ...args,
18428
+ self
18429
+ };
18382
18430
  if (typeof target === "object") return {
18383
18431
  ...args,
18384
18432
  self,
@@ -18944,10 +18992,21 @@ var WebRTCCall = class extends Destroyable {
18944
18992
  return this.deferEmission(this._answered$.asObservable());
18945
18993
  }
18946
18994
  /**
18947
- * Sets the call layout and participant positions.
18995
+ * Sets the call layout and, optionally, individual participant positions.
18996
+ *
18997
+ * The gateway `call.layout.set` DTO has **no** `positions` member, so when
18998
+ * `positions` is provided this method issues a `call.member.position.set`
18999
+ * request per member (via {@link Participant.setPosition}, which keys each
19000
+ * position by that member's own call context) alongside `call.layout.set`
19001
+ * (issue #19400, Flag #6).
19002
+ *
19003
+ * **These operations are NOT atomic.** The layout is applied first, then each
19004
+ * member position sequentially, so members may briefly flash into their
19005
+ * default slots before being moved to the requested positions.
18948
19006
  *
18949
19007
  * @param layout - Layout name (must be one of {@link layouts}).
18950
- * @param positions - Map of member IDs to {@link VideoPosition} values.
19008
+ * @param positions - Optional map of member IDs to {@link VideoPosition} values.
19009
+ * When omitted or empty, only the layout is changed.
18951
19010
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
18952
19011
  *
18953
19012
  * @example
@@ -18960,10 +19019,17 @@ var WebRTCCall = class extends Destroyable {
18960
19019
  async setLayout(layout, positions) {
18961
19020
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
18962
19021
  const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
18963
- await this.executeMethod(selfId, "call.layout.set", {
18964
- layout,
18965
- positions
18966
- });
19022
+ await this.executeMethod(selfId, "call.layout.set", { layout });
19023
+ const positionEntries = Object.entries(positions ?? {});
19024
+ if (positionEntries.length === 0) return;
19025
+ for (const [memberId, position] of positionEntries) {
19026
+ const participant = this.participants.find((p) => p.id === memberId);
19027
+ if (!participant) {
19028
+ logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
19029
+ continue;
19030
+ }
19031
+ await participant.setPosition(position);
19032
+ }
18967
19033
  }
18968
19034
  /**
18969
19035
  * Transfers the call to another destination.
@@ -19882,11 +19948,41 @@ var ClientSessionManager = class extends Destroyable {
19882
19948
  }
19883
19949
  } else this._errors$.next(error);
19884
19950
  }
19951
+ /**
19952
+ * Clear the resume state (authorization_state + protocol) only.
19953
+ *
19954
+ * This is the stale-auth-state recovery helper used by handleAuthError:
19955
+ * the server rejected a reconnect, so the resume state is discarded and a
19956
+ * fresh connect follows. Attach records are deliberately preserved — the
19957
+ * session lives on through the reconnect and reattachCalls() needs the
19958
+ * stored call references afterwards. Do NOT add detachAll() here.
19959
+ *
19960
+ * For public teardown (disconnect/destroy), use {@link teardownSessionState}
19961
+ * instead, which clears the attach records as well.
19962
+ */
19885
19963
  async cleanupStoredConnectionParams() {
19886
19964
  await this.transport.setProtocol(void 0);
19887
19965
  await this.updateAuthorizationStateInStorage(void 0);
19888
19966
  this._authorization$.next(void 0);
19889
19967
  }
19968
+ /**
19969
+ * Public-teardown helper for disconnect()/destroy(). Clears the resume
19970
+ * state (authorization_state + protocol) AND the attach records as one
19971
+ * atomic unit.
19972
+ *
19973
+ * The two stores are coupled: the backend only honors attach records
19974
+ * within the session identified by the resume state, so ending the
19975
+ * session must clear both. Clearing one without the other strands records
19976
+ * no future session can honor (disconnect) or revives a session the
19977
+ * developer explicitly ended (destroy).
19978
+ *
19979
+ * Distinct from {@link cleanupStoredConnectionParams}, which keeps the
19980
+ * attach records for the stale-auth-state recovery path.
19981
+ */
19982
+ async teardownSessionState() {
19983
+ await this.cleanupStoredConnectionParams();
19984
+ await this.attachManager.detachAll();
19985
+ }
19890
19986
  async updateAuthState(authorization_state) {
19891
19987
  try {
19892
19988
  await this.storage.setItem(this.authorizationStateKey, authorization_state);
@@ -19981,7 +20077,7 @@ var ClientSessionManager = class extends Destroyable {
19981
20077
  async disconnect() {
19982
20078
  this.transport.disconnect();
19983
20079
  this._authState$.next({ kind: "unauthenticated" });
19984
- await this.cleanupStoredConnectionParams();
20080
+ await this.teardownSessionState();
19985
20081
  }
19986
20082
  async createInboundCall(invite) {
19987
20083
  const callSession = await this.createCall({
@@ -21148,6 +21244,10 @@ var TransportManager = class extends Destroyable {
21148
21244
  if (!isSignalwireRequest(message)) return true;
21149
21245
  const eventChannel = message.params.event_channel;
21150
21246
  if (!eventChannel) return true;
21247
+ if (message.params.event_type.startsWith("conversation.")) {
21248
+ logger$2.debug(`[Transport] Received conversation event: ${message.params.event_type} (event_channel: ${eventChannel})`);
21249
+ return true;
21250
+ }
21151
21251
  const currentProtocol = this._currentProtocol;
21152
21252
  if (!currentProtocol) return true;
21153
21253
  if (!eventChannel.includes(currentProtocol)) {
@@ -21748,7 +21848,7 @@ var SignalWire = class extends Destroyable {
21748
21848
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
21749
21849
  }
21750
21850
  try {
21751
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.0" });
21851
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.1" });
21752
21852
  } catch (error) {
21753
21853
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
21754
21854
  }
@@ -21756,6 +21856,14 @@ var SignalWire = class extends Destroyable {
21756
21856
  /**
21757
21857
  * Disconnects the WebSocket and tears down the current session.
21758
21858
  *
21859
+ * Ends the session identified by the protocol and clears its persisted
21860
+ * resume state (`authorization_state` + protocol) and attach records
21861
+ * together — a later {@link connect} with the same credentials starts a
21862
+ * fresh session and cannot reattach to the ended session's calls.
21863
+ * Credentials and device preferences are preserved. To temporarily stop
21864
+ * receiving inbound calls while keeping the session alive, use
21865
+ * `unregister()` instead.
21866
+ *
21759
21867
  * The client can be reconnected by calling {@link connect} again,
21760
21868
  * which creates a fresh transport and session.
21761
21869
  */
@@ -22147,12 +22255,20 @@ var SignalWire = class extends Destroyable {
22147
22255
  prefs.preferredVideoInput = null;
22148
22256
  await this._deviceController.clearDeviceState();
22149
22257
  }
22150
- /** Destroys the client, clearing timers and releasing all resources. */
22258
+ /**
22259
+ * Destroys the client, clearing timers and releasing all resources.
22260
+ *
22261
+ * Intentionally destroying the client ends its session: the resume state
22262
+ * (`authorization_state` + protocol) and the attach records are both
22263
+ * cleared. Credentials and device preferences are preserved — use
22264
+ * {@link resetToDefaults} for a full wipe. To temporarily stop receiving
22265
+ * inbound calls while keeping the session alive, use `unregister()`.
22266
+ */
22151
22267
  destroy() {
22152
22268
  this._refreshCoordinator?.destroy();
22153
22269
  this._refreshCoordinator = void 0;
22154
22270
  this._dpopManager?.destroy();
22155
- if (this._attachManager) this._attachManager.detachAll();
22271
+ this._clientSession.teardownSessionState();
22156
22272
  this._transport.destroy();
22157
22273
  this._clientSession.destroy();
22158
22274
  try {
@@ -22267,7 +22383,7 @@ var StaticCredentialProvider = class {
22267
22383
  /**
22268
22384
  * Library version from package.json, injected at build time.
22269
22385
  */
22270
- const version = "4.0.0-rc.0";
22386
+ const version = "4.0.0-rc.1";
22271
22387
  /**
22272
22388
  * Flag indicating the library has been loaded and is ready to use.
22273
22389
  * For UMD builds: `window.SignalWire.ready`
@@ -22289,7 +22405,7 @@ const ready = true;
22289
22405
  */
22290
22406
  const emitReadyEvent = () => {
22291
22407
  if (typeof window !== "undefined") {
22292
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.0" } });
22408
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.1" } });
22293
22409
  window.dispatchEvent(event);
22294
22410
  }
22295
22411
  };