@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/index.mjs CHANGED
@@ -3005,7 +3005,9 @@ const DEFAULT_MEMBER_CAPABILITIES = {
3005
3005
  position: false,
3006
3006
  meta: false,
3007
3007
  remove: false,
3008
- audioFlags: false
3008
+ audioFlags: false,
3009
+ denoise: false,
3010
+ lowbitrate: false
3009
3011
  };
3010
3012
  /**
3011
3013
  * Default call capabilities with no permissions
@@ -3078,7 +3080,9 @@ function computeMemberCapabilities(flags, memberType) {
3078
3080
  position: hasBooleanCapability(flags, memberType, null, "position"),
3079
3081
  meta: hasBooleanCapability(flags, memberType, null, "meta"),
3080
3082
  remove: hasBooleanCapability(flags, memberType, null, "remove"),
3081
- audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags")
3083
+ audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags"),
3084
+ denoise: hasBooleanCapability(flags, memberType, null, "denoise"),
3085
+ lowbitrate: hasBooleanCapability(flags, memberType, null, "lowbitrate")
3082
3086
  };
3083
3087
  }
3084
3088
  /**
@@ -3459,6 +3463,10 @@ var Participant = class extends Destroyable {
3459
3463
  get nodeId() {
3460
3464
  return this._state$.value.node_id;
3461
3465
  }
3466
+ /** Call ID for this participant's leg, or `undefined` if not available. */
3467
+ get callId() {
3468
+ return this._state$.value.call_id;
3469
+ }
3462
3470
  /** @internal */
3463
3471
  get value() {
3464
3472
  return this._state$.value;
@@ -3520,8 +3528,9 @@ var Participant = class extends Destroyable {
3520
3528
  noise_suppression: !this.noiseSuppression
3521
3529
  });
3522
3530
  }
3531
+ /** Toggles low-bitrate mode for this participant's media. */
3523
3532
  async toggleLowbitrate() {
3524
- throw new UnimplementedError();
3533
+ await this.executeMethod(this.id, "call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
3525
3534
  }
3526
3535
  /**
3527
3536
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -3568,10 +3577,27 @@ var Participant = class extends Destroyable {
3568
3577
  }
3569
3578
  /**
3570
3579
  * Sets the participant's position in the video layout.
3580
+ *
3581
+ * Requires the `member.position` capability. The gateway keys positions by the
3582
+ * **target member's own** `call_id`/`node_id` (see issue #19400 and the legacy
3583
+ * `setPositions` implementation), so this sends the participant's own call
3584
+ * context — matching {@link Participant.remove}. A resolved promise does not
3585
+ * guarantee a visible change: the backend silently returns `200` (no-op) for
3586
+ * non-conference targets.
3587
+ *
3571
3588
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
3572
3589
  */
3573
3590
  async setPosition(value) {
3574
- await this.executeMethod(this.id, "call.member.position.set", { position: value });
3591
+ const state = this._state$.value;
3592
+ const target = {
3593
+ member_id: this.id,
3594
+ call_id: state.call_id ?? "",
3595
+ node_id: state.node_id ?? ""
3596
+ };
3597
+ await this.executeMethod(target, "call.member.position.set", { targets: [{
3598
+ target,
3599
+ position: value
3600
+ }] });
3575
3601
  }
3576
3602
  /** Removes this participant from the call. */
3577
3603
  async remove() {
@@ -5979,7 +6005,11 @@ function isVertoInviteMessage(value) {
5979
6005
  const msg = value;
5980
6006
  return msg.method === "verto.invite" && isObject(msg.params) && hasProperty(msg.params, "sdp") && hasProperty(msg.params, "callID");
5981
6007
  }
5982
- function isVertoByeMessage(value) {
6008
+ /**
6009
+ * Guards inbound `verto.bye` frames (server → client). Outbound bye messages are
6010
+ * built locally and never type-guarded, so only the inbound shape is asserted.
6011
+ */
6012
+ function isVertoByeInboundMessage(value) {
5983
6013
  if (!isVertoMethodMessage(value)) return false;
5984
6014
  return value.method === "verto.bye";
5985
6015
  }
@@ -5999,6 +6029,14 @@ function isVertoMediaParamsInnerParams(value) {
5999
6029
  function isVertoPingInnerParams(value) {
6000
6030
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "method") && value.method === "verto.ping";
6001
6031
  }
6032
+ /**
6033
+ * Guards the params of an inbound `verto.bye` frame (top-level `callID`). Use
6034
+ * this to recognise the bye payload extracted by `vertoBye$`, e.g. when racing
6035
+ * it against a boolean answer/reject signal.
6036
+ */
6037
+ function isVertoByeInboundParamsGuard(value) {
6038
+ return isObject(value) && hasProperty(value, "callID") && typeof value.callID === "string";
6039
+ }
6002
6040
 
6003
6041
  //#endregion
6004
6042
  //#region src/managers/VertoManager.ts
@@ -6336,7 +6374,7 @@ var WebRTCVertoManager = class extends VertoManager {
6336
6374
  return this.cachedObservable("vertoMediaParams$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaParamsInnerParams, "params"), takeUntil(this.destroyed$)));
6337
6375
  }
6338
6376
  get vertoBye$() {
6339
- return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeMessage, "params"), takeUntil(this.destroyed$)));
6377
+ return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeInboundMessage, "params"), takeUntil(this.destroyed$)));
6340
6378
  }
6341
6379
  get vertoAttach$() {
6342
6380
  return this.cachedObservable("vertoAttach$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoAttachMessage, "params"), takeUntil(this.destroyed$)));
@@ -6489,7 +6527,7 @@ var WebRTCVertoManager = class extends VertoManager {
6489
6527
  logger$16.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
6490
6528
  return;
6491
6529
  }
6492
- if (isVertoByeMessage(vertoByeOrAccepted)) {
6530
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
6493
6531
  logger$16.info("[WebRTCManager] Inbound call ended by remote before answer.");
6494
6532
  this.callSession?.destroy();
6495
6533
  } else if (!vertoByeOrAccepted) {
@@ -6591,7 +6629,7 @@ var WebRTCVertoManager = class extends VertoManager {
6591
6629
  logger$16.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
6592
6630
  return;
6593
6631
  }
6594
- if (isVertoByeMessage(vertoByeOrAccepted)) {
6632
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
6595
6633
  logger$16.info("[WebRTCManager] Call ended before answer was sent.");
6596
6634
  this.callSession?.destroy();
6597
6635
  } else if (!vertoByeOrAccepted) {
@@ -7646,6 +7684,12 @@ function mosToQualityLevel(mos) {
7646
7684
  //#region src/core/entities/Call.ts
7647
7685
  const logger$12 = getLogger();
7648
7686
  /**
7687
+ * Verto method for setting member layout positions. Its gateway DTO requires a
7688
+ * `targets` array whose entries are `{ target, position }` (NOT bare targets),
7689
+ * so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
7690
+ */
7691
+ const POSITION_SET_METHOD = "call.member.position.set";
7692
+ /**
7649
7693
  * Ratio between the critical and warning RTT spike multipliers.
7650
7694
  * Warning threshold = baseline * warningMultiplier (default 3x)
7651
7695
  * Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
@@ -7852,7 +7896,7 @@ var WebRTCCall = class extends Destroyable {
7852
7896
  * @throws {JSONRPCError} If the RPC call returns an error.
7853
7897
  */
7854
7898
  async executeMethod(target, method, args) {
7855
- const params = this.buildMethodParams(target, args);
7899
+ const params = this.buildMethodParams(target, args, method);
7856
7900
  const request = buildRPCRequest({
7857
7901
  method,
7858
7902
  params
@@ -7866,12 +7910,16 @@ var WebRTCCall = class extends Destroyable {
7866
7910
  throw error;
7867
7911
  }
7868
7912
  }
7869
- buildMethodParams(target, args) {
7913
+ buildMethodParams(target, args, method) {
7870
7914
  const self = {
7871
7915
  node_id: this.nodeId ?? "",
7872
7916
  call_id: this.id,
7873
7917
  member_id: this.vertoManager.selfId ?? ""
7874
7918
  };
7919
+ if (method === POSITION_SET_METHOD) return {
7920
+ ...args,
7921
+ self
7922
+ };
7875
7923
  if (typeof target === "object") return {
7876
7924
  ...args,
7877
7925
  self,
@@ -8437,10 +8485,21 @@ var WebRTCCall = class extends Destroyable {
8437
8485
  return this.deferEmission(this._answered$.asObservable());
8438
8486
  }
8439
8487
  /**
8440
- * Sets the call layout and participant positions.
8488
+ * Sets the call layout and, optionally, individual participant positions.
8489
+ *
8490
+ * The gateway `call.layout.set` DTO has **no** `positions` member, so when
8491
+ * `positions` is provided this method issues a `call.member.position.set`
8492
+ * request per member (via {@link Participant.setPosition}, which keys each
8493
+ * position by that member's own call context) alongside `call.layout.set`
8494
+ * (issue #19400, Flag #6).
8495
+ *
8496
+ * **These operations are NOT atomic.** The layout is applied first, then each
8497
+ * member position sequentially, so members may briefly flash into their
8498
+ * default slots before being moved to the requested positions.
8441
8499
  *
8442
8500
  * @param layout - Layout name (must be one of {@link layouts}).
8443
- * @param positions - Map of member IDs to {@link VideoPosition} values.
8501
+ * @param positions - Optional map of member IDs to {@link VideoPosition} values.
8502
+ * When omitted or empty, only the layout is changed.
8444
8503
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
8445
8504
  *
8446
8505
  * @example
@@ -8453,10 +8512,17 @@ var WebRTCCall = class extends Destroyable {
8453
8512
  async setLayout(layout, positions) {
8454
8513
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
8455
8514
  const selfId = await firstValueFrom(this.selfId$.pipe(filter((id) => id !== null)));
8456
- await this.executeMethod(selfId, "call.layout.set", {
8457
- layout,
8458
- positions
8459
- });
8515
+ await this.executeMethod(selfId, "call.layout.set", { layout });
8516
+ const positionEntries = Object.entries(positions ?? {});
8517
+ if (positionEntries.length === 0) return;
8518
+ for (const [memberId, position] of positionEntries) {
8519
+ const participant = this.participants.find((p) => p.id === memberId);
8520
+ if (!participant) {
8521
+ logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
8522
+ continue;
8523
+ }
8524
+ await participant.setPosition(position);
8525
+ }
8460
8526
  }
8461
8527
  /**
8462
8528
  * Transfers the call to another destination.
@@ -9371,11 +9437,41 @@ var ClientSessionManager = class extends Destroyable {
9371
9437
  }
9372
9438
  } else this._errors$.next(error);
9373
9439
  }
9440
+ /**
9441
+ * Clear the resume state (authorization_state + protocol) only.
9442
+ *
9443
+ * This is the stale-auth-state recovery helper used by handleAuthError:
9444
+ * the server rejected a reconnect, so the resume state is discarded and a
9445
+ * fresh connect follows. Attach records are deliberately preserved — the
9446
+ * session lives on through the reconnect and reattachCalls() needs the
9447
+ * stored call references afterwards. Do NOT add detachAll() here.
9448
+ *
9449
+ * For public teardown (disconnect/destroy), use {@link teardownSessionState}
9450
+ * instead, which clears the attach records as well.
9451
+ */
9374
9452
  async cleanupStoredConnectionParams() {
9375
9453
  await this.transport.setProtocol(void 0);
9376
9454
  await this.updateAuthorizationStateInStorage(void 0);
9377
9455
  this._authorization$.next(void 0);
9378
9456
  }
9457
+ /**
9458
+ * Public-teardown helper for disconnect()/destroy(). Clears the resume
9459
+ * state (authorization_state + protocol) AND the attach records as one
9460
+ * atomic unit.
9461
+ *
9462
+ * The two stores are coupled: the backend only honors attach records
9463
+ * within the session identified by the resume state, so ending the
9464
+ * session must clear both. Clearing one without the other strands records
9465
+ * no future session can honor (disconnect) or revives a session the
9466
+ * developer explicitly ended (destroy).
9467
+ *
9468
+ * Distinct from {@link cleanupStoredConnectionParams}, which keeps the
9469
+ * attach records for the stale-auth-state recovery path.
9470
+ */
9471
+ async teardownSessionState() {
9472
+ await this.cleanupStoredConnectionParams();
9473
+ await this.attachManager.detachAll();
9474
+ }
9379
9475
  async updateAuthState(authorization_state) {
9380
9476
  try {
9381
9477
  await this.storage.setItem(this.authorizationStateKey, authorization_state);
@@ -9470,7 +9566,7 @@ var ClientSessionManager = class extends Destroyable {
9470
9566
  async disconnect() {
9471
9567
  this.transport.disconnect();
9472
9568
  this._authState$.next({ kind: "unauthenticated" });
9473
- await this.cleanupStoredConnectionParams();
9569
+ await this.teardownSessionState();
9474
9570
  }
9475
9571
  async createInboundCall(invite) {
9476
9572
  const callSession = await this.createCall({
@@ -10632,6 +10728,10 @@ var TransportManager = class extends Destroyable {
10632
10728
  if (!isSignalwireRequest(message)) return true;
10633
10729
  const eventChannel = message.params.event_channel;
10634
10730
  if (!eventChannel) return true;
10731
+ if (message.params.event_type.startsWith("conversation.")) {
10732
+ logger$2.debug(`[Transport] Received conversation event: ${message.params.event_type} (event_channel: ${eventChannel})`);
10733
+ return true;
10734
+ }
10635
10735
  const currentProtocol = this._currentProtocol;
10636
10736
  if (!currentProtocol) return true;
10637
10737
  if (!eventChannel.includes(currentProtocol)) {
@@ -11231,7 +11331,7 @@ var SignalWire = class extends Destroyable {
11231
11331
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
11232
11332
  }
11233
11333
  try {
11234
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.0" });
11334
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.1" });
11235
11335
  } catch (error) {
11236
11336
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
11237
11337
  }
@@ -11239,6 +11339,14 @@ var SignalWire = class extends Destroyable {
11239
11339
  /**
11240
11340
  * Disconnects the WebSocket and tears down the current session.
11241
11341
  *
11342
+ * Ends the session identified by the protocol and clears its persisted
11343
+ * resume state (`authorization_state` + protocol) and attach records
11344
+ * together — a later {@link connect} with the same credentials starts a
11345
+ * fresh session and cannot reattach to the ended session's calls.
11346
+ * Credentials and device preferences are preserved. To temporarily stop
11347
+ * receiving inbound calls while keeping the session alive, use
11348
+ * `unregister()` instead.
11349
+ *
11242
11350
  * The client can be reconnected by calling {@link connect} again,
11243
11351
  * which creates a fresh transport and session.
11244
11352
  */
@@ -11630,12 +11738,20 @@ var SignalWire = class extends Destroyable {
11630
11738
  prefs.preferredVideoInput = null;
11631
11739
  await this._deviceController.clearDeviceState();
11632
11740
  }
11633
- /** Destroys the client, clearing timers and releasing all resources. */
11741
+ /**
11742
+ * Destroys the client, clearing timers and releasing all resources.
11743
+ *
11744
+ * Intentionally destroying the client ends its session: the resume state
11745
+ * (`authorization_state` + protocol) and the attach records are both
11746
+ * cleared. Credentials and device preferences are preserved — use
11747
+ * {@link resetToDefaults} for a full wipe. To temporarily stop receiving
11748
+ * inbound calls while keeping the session alive, use `unregister()`.
11749
+ */
11634
11750
  destroy() {
11635
11751
  this._refreshCoordinator?.destroy();
11636
11752
  this._refreshCoordinator = void 0;
11637
11753
  this._dpopManager?.destroy();
11638
- if (this._attachManager) this._attachManager.detachAll();
11754
+ this._clientSession.teardownSessionState();
11639
11755
  this._transport.destroy();
11640
11756
  this._clientSession.destroy();
11641
11757
  try {
@@ -11749,7 +11865,7 @@ var StaticCredentialProvider = class {
11749
11865
  /**
11750
11866
  * Library version from package.json, injected at build time.
11751
11867
  */
11752
- const version = "4.0.0-rc.0";
11868
+ const version = "4.0.0-rc.1";
11753
11869
  /**
11754
11870
  * Flag indicating the library has been loaded and is ready to use.
11755
11871
  * For UMD builds: `window.SignalWire.ready`
@@ -11771,7 +11887,7 @@ const ready = true;
11771
11887
  */
11772
11888
  const emitReadyEvent = () => {
11773
11889
  if (typeof window !== "undefined") {
11774
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.0" } });
11890
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.1" } });
11775
11891
  window.dispatchEvent(event);
11776
11892
  }
11777
11893
  };