@signalwire/js 4.0.0-beta.8 → 4.0.0-beta.9

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.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_operators = require('./operators-CJEML6aa.cjs');
1
+ const require_operators = require('./operators-mm21prWr.cjs');
2
2
  let jwt_decode = require("jwt-decode");
3
3
  let rxjs = require("rxjs");
4
4
  let uuid = require("uuid");
@@ -27,6 +27,40 @@ var Destroyable = class {
27
27
  }
28
28
  return cached;
29
29
  }
30
+ /**
31
+ * Like `cachedObservable`, but defers emissions to the microtask queue
32
+ * via `observeOn(asapScheduler)`.
33
+ *
34
+ * Use ONLY for public-facing observable getters that external consumers
35
+ * subscribe to. Prevents a class of bugs where `BehaviorSubject` or
36
+ * `ReplaySubject` replays synchronously during `subscribe()`, before
37
+ * the subscription variable is assigned in the caller's scope.
38
+ *
39
+ * Do NOT use for observables consumed internally by the SDK — internal
40
+ * code using `subscribeTo()`, `firstValueFrom()`, or `withLatestFrom()`
41
+ * depends on synchronous emission delivery.
42
+ */
43
+ publicCachedObservable(key, factory) {
44
+ const publicKey = `public:${key}`;
45
+ this._observableCache ??= /* @__PURE__ */ new Map();
46
+ let cached = this._observableCache.get(publicKey);
47
+ if (!cached) {
48
+ cached = factory().pipe((0, rxjs.observeOn)(rxjs.asapScheduler));
49
+ this._observableCache.set(publicKey, cached);
50
+ }
51
+ return cached;
52
+ }
53
+ /**
54
+ * Wraps an observable so emissions are deferred to the microtask queue.
55
+ *
56
+ * Use ONLY for public-facing getters that expose a subject via
57
+ * `.asObservable()` without going through `cachedObservable`.
58
+ *
59
+ * Do NOT use for observables consumed internally by the SDK.
60
+ */
61
+ deferEmission(observable) {
62
+ return observable.pipe((0, rxjs.observeOn)(rxjs.asapScheduler));
63
+ }
30
64
  subscribeTo(observable, observerOrNext) {
31
65
  const subscription = observable.subscribe(observerOrNext);
32
66
  this.subscriptions.push(subscription);
@@ -1231,17 +1265,21 @@ var AttachManager = class {
1231
1265
  buildCallOptions(attachment) {
1232
1266
  const { audio: audioDirection, video: videoDirection } = attachment.mediaDirections;
1233
1267
  const { audioInputDevice, videoInputDevice } = attachment;
1268
+ const receiveAudio = audioDirection.includes("recv");
1269
+ const receiveVideo = videoDirection.includes("recv");
1270
+ const sendAudio = audioDirection.includes("send");
1271
+ const sendVideo = videoDirection.includes("send");
1234
1272
  return {
1235
- receiveAudio: audioDirection.includes("recv"),
1236
- receiveVideo: videoDirection.includes("recv"),
1237
- inputAudioDeviceConstraints: {
1238
- audio: audioDirection.includes("send"),
1273
+ receiveAudio,
1274
+ receiveVideo,
1275
+ inputAudioDeviceConstraints: sendAudio ? {
1276
+ audio: true,
1239
1277
  ...this.deviceController.deviceInfoToConstraints(audioInputDevice)
1240
- },
1241
- inputVideoDeviceConstraints: {
1242
- video: videoDirection.includes("send"),
1278
+ } : void 0,
1279
+ inputVideoDeviceConstraints: sendVideo ? {
1280
+ video: true,
1243
1281
  ...this.deviceController.deviceInfoToConstraints(videoInputDevice)
1244
- },
1282
+ } : void 0,
1245
1283
  reattach: true
1246
1284
  };
1247
1285
  }
@@ -1947,6 +1985,9 @@ function isJSONRPCRequest(value) {
1947
1985
  function isJSONRPCResponse(value) {
1948
1986
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "id") && typeof value.id === "string" && (hasProperty(value, "result") || hasProperty(value, "error"));
1949
1987
  }
1988
+ function isJSONRPCErrorResponse(value) {
1989
+ return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "id") && typeof value.id === "string" && (hasProperty(value, "error") && isObject(value.error) && hasProperty(value.error, "code") && hasProperty(value.error, "message") || hasProperty(value, "result") && isObject(value.result) && hasProperty(value.result, "code") && value.result.code !== "200" && hasProperty(value.result, "message"));
1990
+ }
1950
1991
 
1951
1992
  //#endregion
1952
1993
  //#region src/core/RPCMessages/guards/events.guards.ts
@@ -3668,6 +3709,11 @@ var WebRTCVertoManager = class extends VertoManager {
3668
3709
  ].includes(connectionState)))));
3669
3710
  }
3670
3711
  initSubscriptions() {
3712
+ this.subscribeTo(this.callJoinedEvent$, (event) => {
3713
+ const memberNodeId = event.room_session.members.find((m) => m.call_id === event.call_id)?.node_id;
3714
+ if (memberNodeId) this.setNodeIdIfNull(memberNodeId);
3715
+ if (event.member_id) this.setSelfIdIfNull(event.member_id);
3716
+ });
3671
3717
  this.subscribeTo(this.vertoMedia$, (event) => {
3672
3718
  logger$10.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
3673
3719
  this._signalingStatus$.next("ringing");
@@ -3699,6 +3745,28 @@ var WebRTCVertoManager = class extends VertoManager {
3699
3745
  this.sendVertoPong(vertoPing);
3700
3746
  });
3701
3747
  }
3748
+ /**
3749
+ * Set node_id/selfId only when the current value is null.
3750
+ *
3751
+ * During reattach, `call.joined` and `verto.answer` events can deliver
3752
+ * these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
3753
+ * arrives. These methods let early events populate them eagerly so that
3754
+ * downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
3755
+ * identifiers. `processInviteResponse()` remains the authoritative source
3756
+ * and always overwrites unconditionally.
3757
+ */
3758
+ setNodeIdIfNull(nodeId) {
3759
+ if (!this._nodeId$.value && nodeId) {
3760
+ logger$10.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
3761
+ this._nodeId$.next(nodeId);
3762
+ }
3763
+ }
3764
+ setSelfIdIfNull(selfId) {
3765
+ if (!this._selfId$.value && selfId) {
3766
+ logger$10.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
3767
+ this._selfId$.next(selfId);
3768
+ }
3769
+ }
3702
3770
  async sendVertoPong(vertoPing) {
3703
3771
  try {
3704
3772
  const vertoPongMessage = VertoPong({ ...vertoPing });
@@ -3722,6 +3790,9 @@ var WebRTCVertoManager = class extends VertoManager {
3722
3790
  get selfId() {
3723
3791
  return this._selfId$.value;
3724
3792
  }
3793
+ get callJoinedEvent$() {
3794
+ return this.webRtcCallSession.callEvent$.pipe((0, rxjs.filter)(isCallJoinedPayload), (0, rxjs.takeUntil)(this.destroyed$));
3795
+ }
3725
3796
  get vertoMedia$() {
3726
3797
  return this.webRtcCallSession.webrtcMessages$.pipe(require_operators.filterAs(isVertoMediaInnerParams, "params"), (0, rxjs.takeUntil)(this.destroyed$));
3727
3798
  }
@@ -4252,7 +4323,7 @@ var WebRTCCall = class extends Destroyable {
4252
4323
  }
4253
4324
  /** Observable stream of errors from media, signaling, and peer connection layers. */
4254
4325
  get errors$() {
4255
- return this._errors$.asObservable();
4326
+ return this.deferEmission(this._errors$.asObservable());
4256
4327
  }
4257
4328
  /**
4258
4329
  * @internal Push an error to the call's error stream.
@@ -4272,7 +4343,7 @@ var WebRTCCall = class extends Destroyable {
4272
4343
  }
4273
4344
  /** Observable of the address associated with this call. */
4274
4345
  get address$() {
4275
- return (0, rxjs.from)([this.address]);
4346
+ return this.deferEmission((0, rxjs.from)([this.address])).pipe((0, rxjs.takeUntil)(this._destroyed$));
4276
4347
  }
4277
4348
  /** Display name of the caller. */
4278
4349
  get fromName() {
@@ -4333,7 +4404,7 @@ var WebRTCCall = class extends Destroyable {
4333
4404
  }
4334
4405
  /** Observable of layout layer positions for all participants. */
4335
4406
  get layoutLayers$() {
4336
- return this.callEventsManager.layoutLayers$;
4407
+ return this.deferEmission(this.callEventsManager.layoutLayers$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4337
4408
  }
4338
4409
  /** Current snapshot of layout layers. */
4339
4410
  get layoutLayers() {
@@ -4347,7 +4418,9 @@ var WebRTCCall = class extends Destroyable {
4347
4418
  params
4348
4419
  });
4349
4420
  try {
4350
- return await this.clientSession.execute(request);
4421
+ const response = await this.clientSession.execute(request);
4422
+ if (isJSONRPCErrorResponse(response)) throw new require_operators.JSONRPCError(parseInt(response.result?.code ?? "0"), `Error response from method ${method}: ${response.result?.code} ${response.result?.message}`, void 0, void 0, request.id);
4423
+ return response;
4351
4424
  } catch (error) {
4352
4425
  logger$9.error(`[Call] Error executing method ${method} with params`, params, error);
4353
4426
  throw error;
@@ -4376,45 +4449,45 @@ var WebRTCCall = class extends Destroyable {
4376
4449
  }
4377
4450
  /** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
4378
4451
  get status$() {
4379
- return this.cachedObservable("status$", () => (0, rxjs.merge)(this._status$.asObservable(), this.vertoManager.signalingStatus$).pipe((0, rxjs.distinctUntilChanged)(), (0, rxjs.tap)((status) => {
4452
+ return this.publicCachedObservable("status$", () => (0, rxjs.merge)(this._status$.asObservable(), this.vertoManager.signalingStatus$).pipe((0, rxjs.distinctUntilChanged)(), (0, rxjs.tap)((status) => {
4380
4453
  this._lastMergedStatus = status;
4381
4454
  })));
4382
4455
  }
4383
4456
  /** Observable of the participants list, emits on join/leave/update. */
4384
4457
  get participants$() {
4385
- return this.callEventsManager.participants$;
4458
+ return this.deferEmission(this.callEventsManager.participants$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4386
4459
  }
4387
4460
  /** Observable of the local (self) participant. */
4388
4461
  get self$() {
4389
- return this.callEventsManager.self$;
4462
+ return this.deferEmission(this.callEventsManager.self$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4390
4463
  }
4391
4464
  /** Observable indicating whether the call is being recorded. */
4392
4465
  get recording$() {
4393
- return this.callEventsManager.recording$;
4466
+ return this.deferEmission(this.callEventsManager.recording$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4394
4467
  }
4395
4468
  /** Observable indicating whether the call is being streamed. */
4396
4469
  get streaming$() {
4397
- return this.callEventsManager.streaming$;
4470
+ return this.deferEmission(this.callEventsManager.streaming$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4398
4471
  }
4399
4472
  /** Observable indicating whether raise-hand priority is active. */
4400
4473
  get raiseHandPriority$() {
4401
- return this.callEventsManager.raiseHandPriority$;
4474
+ return this.deferEmission(this.callEventsManager.raiseHandPriority$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4402
4475
  }
4403
4476
  /** Observable indicating whether the call room is locked. */
4404
4477
  get locked$() {
4405
- return this.callEventsManager.locked$;
4478
+ return this.deferEmission(this.callEventsManager.locked$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4406
4479
  }
4407
4480
  /** Observable of custom metadata associated with the call. */
4408
4481
  get meta$() {
4409
- return this.callEventsManager.meta$;
4482
+ return this.deferEmission(this.callEventsManager.meta$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4410
4483
  }
4411
4484
  /** Observable of the call's capability flags. */
4412
4485
  get capabilities$() {
4413
- return this.callEventsManager.capabilities$;
4486
+ return this.deferEmission(this.callEventsManager.capabilities$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4414
4487
  }
4415
4488
  /** Observable of the current layout name. */
4416
4489
  get layout$() {
4417
- return this.callEventsManager.layout$;
4490
+ return this.deferEmission(this.callEventsManager.layout$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4418
4491
  }
4419
4492
  /** Current call status. */
4420
4493
  get status() {
@@ -4446,7 +4519,7 @@ var WebRTCCall = class extends Destroyable {
4446
4519
  }
4447
4520
  /** Observable of available layout names. */
4448
4521
  get layouts$() {
4449
- return this.callEventsManager.layouts$;
4522
+ return this.deferEmission(this.callEventsManager.layouts$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4450
4523
  }
4451
4524
  /** Current snapshot of available layout names. */
4452
4525
  get layouts() {
@@ -4454,7 +4527,7 @@ var WebRTCCall = class extends Destroyable {
4454
4527
  }
4455
4528
  /** Observable of the local media stream (camera/microphone). */
4456
4529
  get localStream$() {
4457
- return this.vertoManager.localStream$;
4530
+ return this.deferEmission(this.vertoManager.localStream$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4458
4531
  }
4459
4532
  /** Current local media stream, or `null` if not available. */
4460
4533
  get localStream() {
@@ -4462,7 +4535,7 @@ var WebRTCCall = class extends Destroyable {
4462
4535
  }
4463
4536
  /** Observable of the remote media stream from the far end. */
4464
4537
  get remoteStream$() {
4465
- return this.vertoManager.remoteStream$;
4538
+ return this.deferEmission(this.vertoManager.remoteStream$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4466
4539
  }
4467
4540
  /** Current remote media stream, or `null` if not available. */
4468
4541
  get remoteStream() {
@@ -4470,7 +4543,7 @@ var WebRTCCall = class extends Destroyable {
4470
4543
  }
4471
4544
  /** Observable of custom user variables associated with the call. */
4472
4545
  get userVariables$() {
4473
- return this._userVariables$.asObservable();
4546
+ return this.deferEmission(this._userVariables$.asObservable());
4474
4547
  }
4475
4548
  /** a copy of the current custom user variables of the call. */
4476
4549
  get userVariables() {
@@ -4490,7 +4563,7 @@ var WebRTCCall = class extends Destroyable {
4490
4563
  }
4491
4564
  /** Observable of the current audio/video send/receive directions. */
4492
4565
  get mediaDirections$() {
4493
- return this.vertoManager.mediaDirections$;
4566
+ return this.deferEmission(this.vertoManager.mediaDirections$).pipe((0, rxjs.takeUntil)(this._destroyed$));
4494
4567
  }
4495
4568
  /** Current audio/video send/receive directions. */
4496
4569
  get mediaDirections() {
@@ -4536,31 +4609,31 @@ var WebRTCCall = class extends Destroyable {
4536
4609
  }
4537
4610
  /** Observable of call-updated events. */
4538
4611
  get callUpdated$() {
4539
- return this.cachedObservable("callUpdated$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isCallUpdatedMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4612
+ return this.publicCachedObservable("callUpdated$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isCallUpdatedMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4540
4613
  }
4541
4614
  /** Observable of member-joined events. */
4542
4615
  get memberJoined$() {
4543
- return this.cachedObservable("memberJoined$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isMemberJoinedMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4616
+ return this.publicCachedObservable("memberJoined$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isMemberJoinedMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4544
4617
  }
4545
4618
  /** Observable of member-left events. */
4546
4619
  get memberLeft$() {
4547
- return this.cachedObservable("memberLeft$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isMemberLeftMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4620
+ return this.publicCachedObservable("memberLeft$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isMemberLeftMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4548
4621
  }
4549
4622
  /** Observable of member-updated events (mute, volume, etc.). */
4550
4623
  get memberUpdated$() {
4551
- return this.cachedObservable("memberUpdated$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isMemberUpdatedMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4624
+ return this.publicCachedObservable("memberUpdated$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isMemberUpdatedMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4552
4625
  }
4553
4626
  /** Observable of member-talking events (speech start/stop). */
4554
4627
  get memberTalking$() {
4555
- return this.cachedObservable("memberTalking$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isMemberTalkingMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4628
+ return this.publicCachedObservable("memberTalking$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isMemberTalkingMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4556
4629
  }
4557
4630
  /** Observable of call state-change events. */
4558
4631
  get callStates$() {
4559
- return this.cachedObservable("callStates$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isCallStateMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4632
+ return this.publicCachedObservable("callStates$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isCallStateMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4560
4633
  }
4561
4634
  /** Observable of layout-changed events. */
4562
4635
  get layoutUpdates$() {
4563
- return this.cachedObservable("layoutUpdates$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isLayoutChangedMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4636
+ return this.publicCachedObservable("layoutUpdates$", () => this.callSessionEvents$.pipe(require_operators.filterAs(isLayoutChangedMetadata, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
4564
4637
  }
4565
4638
  /** Underlying `RTCPeerConnection`, for advanced use cases. */
4566
4639
  get rtcPeerConnection() {
@@ -4568,7 +4641,7 @@ var WebRTCCall = class extends Destroyable {
4568
4641
  }
4569
4642
  /** Observable of raw signaling events as plain objects. */
4570
4643
  get signalingEvent$() {
4571
- return this.cachedObservable("signalingEvent$", () => this.callEvent$.pipe((0, rxjs.map)((event) => JSON.parse(JSON.stringify(event)))));
4644
+ return this.publicCachedObservable("signalingEvent$", () => this.callEvent$.pipe((0, rxjs.map)((event) => JSON.parse(JSON.stringify(event)))));
4572
4645
  }
4573
4646
  /** Observable of WebRTC-specific signaling messages. */
4574
4647
  get webrtcMessages$() {
@@ -4610,7 +4683,7 @@ var WebRTCCall = class extends Destroyable {
4610
4683
  }
4611
4684
  /** Observable that emits `true` when answered, `false` when rejected. */
4612
4685
  get answered$() {
4613
- return this._answered$.asObservable();
4686
+ return this.deferEmission(this._answered$.asObservable());
4614
4687
  }
4615
4688
  /**
4616
4689
  * Sets the call layout and participant positions.
@@ -5064,7 +5137,7 @@ var PendingRPC = class PendingRPC {
5064
5137
  next: (response) => {
5065
5138
  isSettled = true;
5066
5139
  if (response.error) {
5067
- const rpcError = new require_operators.RPCError(response.error.code, request.id, response.error.message, response.error.data);
5140
+ const rpcError = new require_operators.JSONRPCError(response.error.code, response.error.message, response.error.data, void 0, request.id);
5068
5141
  logger$7.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
5069
5142
  reject(rpcError);
5070
5143
  } else {
@@ -5354,6 +5427,7 @@ var ClientSessionManager = class extends Destroyable {
5354
5427
  ...params,
5355
5428
  ...persistedParams
5356
5429
  });
5430
+ this.transport.resetSessionEpoch();
5357
5431
  const response = await (0, rxjs.lastValueFrom)((0, rxjs.from)(this.transport.execute(rpcConnectRequest)).pipe(require_operators.throwOnRPCError(), (0, rxjs.map)((res) => res.result), (0, rxjs.filter)(isRPCConnectResult), (0, rxjs.tap)(() => {
5358
5432
  logger$6.debug("[Session] Response passed filter, processing authentication result");
5359
5433
  }), (0, rxjs.take)(1), (0, rxjs.catchError)((err) => {
@@ -5833,7 +5907,30 @@ function isSignalwirePingRequest(value) {
5833
5907
  //#endregion
5834
5908
  //#region src/managers/TransportManager.ts
5835
5909
  const logger$2 = require_operators.getLogger();
5836
- var TransportManager = class extends Destroyable {
5910
+ var TransportManager = class TransportManager extends Destroyable {
5911
+ /**
5912
+ * Normalise a server event timestamp to epoch seconds.
5913
+ *
5914
+ * The server uses two formats:
5915
+ * - `webrtc.message`: float epoch seconds (e.g. 1774372099.022817)
5916
+ * - all other events: int epoch microseconds (e.g. 1774372099925857)
5917
+ *
5918
+ * Values above 1e12 are treated as microseconds and divided by 1e6.
5919
+ */
5920
+ static toEpochSeconds(ts) {
5921
+ const n = typeof ts === "string" ? Number(ts) : ts;
5922
+ return n > 0xe8d4a51000 ? n / 1e6 : n;
5923
+ }
5924
+ /**
5925
+ * Extract the event timestamp from a signalwire.event message.
5926
+ * Returns `null` for messages that have no timestamp
5927
+ * (e.g. signalwire.authorization.state, RPC responses).
5928
+ */
5929
+ static extractEventTimestamp(message) {
5930
+ if (!isSignalwireRequest(message)) return null;
5931
+ if (message.params.event_type === "signalwire.authorization.state") return null;
5932
+ return TransportManager.toEpochSeconds(message.params.timestamp);
5933
+ }
5837
5934
  constructor(storage, protocolKey, webSocketConstructor, relayHost, onError) {
5838
5935
  super();
5839
5936
  this.storage = storage;
@@ -5866,6 +5963,23 @@ var TransportManager = class extends Destroyable {
5866
5963
  return true;
5867
5964
  });
5868
5965
  };
5966
+ this.discardStaleEvents = () => {
5967
+ return (0, rxjs.filter)((message) => {
5968
+ const ts = TransportManager.extractEventTimestamp(message);
5969
+ if (ts === null) return true;
5970
+ if (this._sessionEpoch === null) {
5971
+ this._sessionEpoch = ts;
5972
+ return true;
5973
+ }
5974
+ if (ts < this._sessionEpoch) {
5975
+ const eventType = isSignalwireRequest(message) ? message.params.event_type : "unknown";
5976
+ logger$2.warn(`[Transport] Discarding stale event: ${eventType} (timestamp=${ts.toFixed(3)}, sessionEpoch=${this._sessionEpoch.toFixed(3)}, delta=${(this._sessionEpoch - ts).toFixed(3)}s)`);
5977
+ return false;
5978
+ }
5979
+ return true;
5980
+ });
5981
+ };
5982
+ this._sessionEpoch = null;
5869
5983
  this._outgoingMessages$ = this.createSubject();
5870
5984
  this._webSocketConnections = new WebSocketController(webSocketConstructor, relayHost, this._outgoingMessages$.asObservable(), {
5871
5985
  connectionTimeout: PreferencesContainer.instance.connectionTimeout,
@@ -5890,7 +6004,14 @@ var TransportManager = class extends Destroyable {
5890
6004
  return rxjs.EMPTY;
5891
6005
  }), (0, rxjs.share)(), (0, rxjs.takeUntil)(this.destroyed$));
5892
6006
  this._jsonRPCResponse$ = this._jsonRPCMessage$.pipe((0, rxjs.filter)(isJSONRPCResponse));
5893
- this._incomingEvent$ = this._jsonRPCMessage$.pipe(this.ackEvent(), this.replySignalwirePing(), (0, rxjs.filter)((message) => !isJSONRPCResponse(message)), (0, rxjs.share)(), (0, rxjs.takeUntil)(this.destroyed$));
6007
+ this._incomingEvent$ = this._jsonRPCMessage$.pipe(this.ackEvent(), this.replySignalwirePing(), (0, rxjs.filter)((message) => !isJSONRPCResponse(message)), this.discardStaleEvents(), (0, rxjs.share)(), (0, rxjs.takeUntil)(this.destroyed$));
6008
+ }
6009
+ /**
6010
+ * Reset the session epoch. Call this before each signalwire.connect
6011
+ * so that the first event after authentication establishes the new baseline.
6012
+ */
6013
+ resetSessionEpoch() {
6014
+ this._sessionEpoch = null;
5894
6015
  }
5895
6016
  async setProtocol(protocol) {
5896
6017
  this.protocol$.next(protocol);
@@ -6213,7 +6334,7 @@ var SignalWire = class extends Destroyable {
6213
6334
  * ```
6214
6335
  */
6215
6336
  get subscriber$() {
6216
- return this._subscriber$.asObservable();
6337
+ return this.deferEmission(this._subscriber$.asObservable());
6217
6338
  }
6218
6339
  /** Current subscriber snapshot, or `undefined` if not yet authenticated. */
6219
6340
  get subscriber() {
@@ -6232,7 +6353,7 @@ var SignalWire = class extends Destroyable {
6232
6353
  * ```
6233
6354
  */
6234
6355
  get directory$() {
6235
- return this._directory$.asObservable();
6356
+ return this.deferEmission(this._directory$.asObservable());
6236
6357
  }
6237
6358
  /**
6238
6359
  * Current directory snapshot, or `undefined` if the client is not yet connected.
@@ -6243,7 +6364,7 @@ var SignalWire = class extends Destroyable {
6243
6364
  }
6244
6365
  /** Observable that emits when the subscriber registration state changes. */
6245
6366
  get isRegistered$() {
6246
- return this._isRegistered$.asObservable();
6367
+ return this.deferEmission(this._isRegistered$.asObservable());
6247
6368
  }
6248
6369
  /** Whether the subscriber is currently registered. */
6249
6370
  get isRegistered() {
@@ -6255,15 +6376,15 @@ var SignalWire = class extends Destroyable {
6255
6376
  }
6256
6377
  /** Observable that emits when the connection state changes. */
6257
6378
  get isConnected$() {
6258
- return this._isConnected$.asObservable();
6379
+ return this.deferEmission(this._isConnected$.asObservable());
6259
6380
  }
6260
6381
  /** Observable that emits `true` when the client is both connected and authenticated. */
6261
6382
  get ready$() {
6262
- return this.cachedObservable("ready$", () => this._isConnected$.pipe((0, rxjs.switchMap)((connected) => connected ? this._clientSession.authenticated$ : (0, rxjs.of)(false))));
6383
+ return this.publicCachedObservable("ready$", () => this._isConnected$.pipe((0, rxjs.switchMap)((connected) => connected ? this._clientSession.authenticated$ : (0, rxjs.of)(false))));
6263
6384
  }
6264
6385
  /** Observable stream of errors from transport, authentication, and devices. */
6265
6386
  get errors$() {
6266
- return this._errors$.asObservable();
6387
+ return this.deferEmission(this._errors$.asObservable());
6267
6388
  }
6268
6389
  /** Disconnects the WebSocket and tears down the session. */
6269
6390
  async disconnect() {
@@ -6338,7 +6459,7 @@ var SignalWire = class extends Destroyable {
6338
6459
  }
6339
6460
  /** Observable list of available audio input (microphone) devices. */
6340
6461
  get audioInputDevices$() {
6341
- return this._deviceController.audioInputDevices$;
6462
+ return this.deferEmission(this._deviceController.audioInputDevices$);
6342
6463
  }
6343
6464
  /** Current snapshot of available audio input devices. */
6344
6465
  get audioInputDevices() {
@@ -6346,7 +6467,7 @@ var SignalWire = class extends Destroyable {
6346
6467
  }
6347
6468
  /** Observable list of available audio output (speaker) devices. */
6348
6469
  get audioOutputDevices$() {
6349
- return this._deviceController.audioOutputDevices$;
6470
+ return this.deferEmission(this._deviceController.audioOutputDevices$);
6350
6471
  }
6351
6472
  /** Current snapshot of available audio output devices. */
6352
6473
  get audioOutputDevices() {
@@ -6354,7 +6475,7 @@ var SignalWire = class extends Destroyable {
6354
6475
  }
6355
6476
  /** Observable list of available video input (camera) devices. */
6356
6477
  get videoInputDevices$() {
6357
- return this._deviceController.videoInputDevices$;
6478
+ return this.deferEmission(this._deviceController.videoInputDevices$);
6358
6479
  }
6359
6480
  /** Current snapshot of available video input devices. */
6360
6481
  get videoInputDevices() {
@@ -6362,15 +6483,15 @@ var SignalWire = class extends Destroyable {
6362
6483
  }
6363
6484
  /** Observable of the currently selected audio input device. */
6364
6485
  get selectedAudioInputDevice$() {
6365
- return this._deviceController.selectedAudioInputDevice$;
6486
+ return this.deferEmission(this._deviceController.selectedAudioInputDevice$);
6366
6487
  }
6367
6488
  /** Observable of the currently selected audio output device. */
6368
6489
  get selectedAudioOutputDevice$() {
6369
- return this._deviceController.selectedAudioOutputDevice$;
6490
+ return this.deferEmission(this._deviceController.selectedAudioOutputDevice$);
6370
6491
  }
6371
6492
  /** Observable of the currently selected video input device. */
6372
6493
  get selectedVideoInputDevice$() {
6373
- return this._deviceController.selectedVideoInputDevice$;
6494
+ return this.deferEmission(this._deviceController.selectedVideoInputDevice$);
6374
6495
  }
6375
6496
  /** Currently selected audio input device, or `null` if none. */
6376
6497
  get selectedAudioInputDevice() {