@sidebandchat/client 0.2.1 → 0.3.0

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.
Files changed (58) hide show
  1. package/dist/activity/activity-sharing-controller.d.ts +2 -0
  2. package/dist/activity/activity-sharing-controller.d.ts.map +1 -1
  3. package/dist/activity/activity-sharing-controller.js +40 -9
  4. package/dist/activity/activity-sharing-controller.js.map +1 -1
  5. package/dist/calls/call-runtime.d.ts +37 -0
  6. package/dist/calls/call-runtime.d.ts.map +1 -0
  7. package/dist/calls/call-runtime.js +2 -0
  8. package/dist/calls/call-runtime.js.map +1 -0
  9. package/dist/calls/index.d.ts +1 -0
  10. package/dist/calls/index.d.ts.map +1 -1
  11. package/dist/calls/media-controls.d.ts +5 -0
  12. package/dist/calls/media-controls.d.ts.map +1 -1
  13. package/dist/calls/media-controls.js +79 -4
  14. package/dist/calls/media-controls.js.map +1 -1
  15. package/dist/calls/media.d.ts +1 -1
  16. package/dist/calls/media.d.ts.map +1 -1
  17. package/dist/calls/media.js +5 -12
  18. package/dist/calls/media.js.map +1 -1
  19. package/dist/client/ephemeral-state.d.ts.map +1 -1
  20. package/dist/client/ephemeral-state.js +31 -23
  21. package/dist/client/ephemeral-state.js.map +1 -1
  22. package/dist/client/index.d.ts +1 -1
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js.map +1 -1
  25. package/dist/client/platform-adapters.d.ts +21 -1
  26. package/dist/client/platform-adapters.d.ts.map +1 -1
  27. package/dist/client/session-runtime.d.ts +35 -1
  28. package/dist/client/session-runtime.d.ts.map +1 -1
  29. package/dist/client/session-runtime.js +428 -36
  30. package/dist/client/session-runtime.js.map +1 -1
  31. package/dist/client/sideband-client.d.ts.map +1 -1
  32. package/dist/client/sideband-client.js +18 -1
  33. package/dist/client/sideband-client.js.map +1 -1
  34. package/dist/communities/community-client.d.ts.map +1 -1
  35. package/dist/communities/community-client.js +11 -2
  36. package/dist/communities/community-client.js.map +1 -1
  37. package/dist/domain.d.ts +3 -2
  38. package/dist/domain.d.ts.map +1 -1
  39. package/dist/domain.js.map +1 -1
  40. package/dist/realtime/realtime-client.d.ts +8 -0
  41. package/dist/realtime/realtime-client.d.ts.map +1 -1
  42. package/dist/realtime/realtime-client.js +9 -0
  43. package/dist/realtime/realtime-client.js.map +1 -1
  44. package/dist/sbp/records.d.ts.map +1 -1
  45. package/dist/sbp/records.js +20 -5
  46. package/dist/sbp/records.js.map +1 -1
  47. package/dist/sbp/value.d.ts.map +1 -1
  48. package/dist/sbp/value.js +1 -1
  49. package/dist/sbp/value.js.map +1 -1
  50. package/dist/social/social-client.d.ts +1 -0
  51. package/dist/social/social-client.d.ts.map +1 -1
  52. package/dist/social/social-client.js +3 -0
  53. package/dist/social/social-client.js.map +1 -1
  54. package/dist/workspace/workspace-controller.d.ts +1 -1
  55. package/dist/workspace/workspace-controller.d.ts.map +1 -1
  56. package/dist/workspace/workspace-controller.js +3 -6
  57. package/dist/workspace/workspace-controller.js.map +1 -1
  58. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { decodeEntityRef } from "../sbp/codec.js";
2
2
  import { decodeCall, decodePrivateConversation, decodePrivateTransfer, } from "../sbp/records.js";
3
+ import { SbpProtocolError } from "../sbp/session.js";
3
4
  import { MediaControls } from "../calls/media-controls.js";
4
5
  import { arrayValue, bytesValue, mapValue, required, unsignedValue, } from "../sbp/value.js";
5
6
  export class SessionRuntime {
@@ -11,8 +12,10 @@ export class SessionRuntime {
11
12
  media;
12
13
  crypto;
13
14
  transferAdapter;
15
+ options;
14
16
  mediaControls;
15
17
  current = {
18
+ callConnection: "idle",
16
19
  calls: new Map(),
17
20
  privateConversations: new Map(),
18
21
  transfers: new Map(),
@@ -20,6 +23,7 @@ export class SessionRuntime {
20
23
  };
21
24
  listeners = new Set();
22
25
  privateEventListeners = new Set();
26
+ callEventListeners = new Set();
23
27
  unsubscribers = [];
24
28
  pending = Promise.resolve();
25
29
  cryptoHandles = new Map();
@@ -28,7 +32,10 @@ export class SessionRuntime {
28
32
  connectingTransfers = new Map();
29
33
  transferKeys = new Map();
30
34
  transferProgress = new Map();
31
- constructor(session, calls, privateConversations, endpoint, ownUserId, media, crypto, transferAdapter, mediaPreferences) {
35
+ connectingAnsweredCalls = new Set();
36
+ watchedCommunityCallIds = new Set();
37
+ mediaGeneration = 0;
38
+ constructor(session, calls, privateConversations, endpoint, ownUserId, media, crypto, transferAdapter, mediaPreferences, options = {}) {
32
39
  this.session = session;
33
40
  this.calls = calls;
34
41
  this.privateConversations = privateConversations;
@@ -37,6 +44,7 @@ export class SessionRuntime {
37
44
  this.media = media;
38
45
  this.crypto = crypto;
39
46
  this.transferAdapter = transferAdapter;
47
+ this.options = options;
40
48
  if (media)
41
49
  this.mediaControls = new MediaControls(media, mediaPreferences);
42
50
  }
@@ -51,6 +59,32 @@ export class SessionRuntime {
51
59
  this.privateEventListeners.add(listener);
52
60
  return () => this.privateEventListeners.delete(listener);
53
61
  };
62
+ subscribeCallEvents = (listener) => {
63
+ this.callEventListeners.add(listener);
64
+ return () => this.callEventListeners.delete(listener);
65
+ };
66
+ callView() {
67
+ const current = this.current.currentCallId
68
+ ? this.current.calls.get(this.current.currentCallId)
69
+ : undefined;
70
+ const incoming = [...this.current.calls.values()].find((call) => openCall(call) &&
71
+ call.id !== current?.id &&
72
+ call.originatorId !== this.ownUserId &&
73
+ ownParticipant(call, this.ownUserId)?.state === "ringing");
74
+ return {
75
+ connection: this.current.callConnection,
76
+ phase: current ? callPhase(current) : undefined,
77
+ current,
78
+ incoming,
79
+ joinable: new Map([...this.current.calls].filter(([, call]) => openCall(call) && call.target.kind !== "user")),
80
+ pendingParticipantIds: current && current.target.kind === "conversation" && openCall(current)
81
+ ? current.participants
82
+ .filter((participant) => participant.userId !== current.originatorId &&
83
+ ["invited", "ringing", "joining"].includes(participant.state))
84
+ .map((participant) => participant.userId)
85
+ : [],
86
+ };
87
+ }
54
88
  privateConversationView(id) {
55
89
  const session = id
56
90
  ? this.current.privateConversations.get(id)
@@ -101,16 +135,23 @@ export class SessionRuntime {
101
135
  if (this.unsubscribers.length)
102
136
  return;
103
137
  const unsubscribeStatus = this.session.subscribeStatus?.((status) => this.receiveStatus(status));
138
+ const unsubscribeMedia = this.media?.subscribeEvents?.((event) => this.receiveMediaEvent(event));
104
139
  this.unsubscribers = [
105
140
  this.session.subscribeEvents((frame) => this.receiveEvent(frame)),
106
141
  this.session.subscribeControls((frame) => this.receiveControl(frame)),
107
142
  ...(unsubscribeStatus ? [unsubscribeStatus] : []),
143
+ ...(unsubscribeMedia ? [unsubscribeMedia] : []),
108
144
  ];
109
145
  void this.mediaControls?.start();
110
146
  }
111
147
  stop() {
148
+ const currentCallId = this.current.currentCallId;
149
+ this.mediaGeneration += 1;
112
150
  this.unsubscribers.splice(0).forEach((unsubscribe) => unsubscribe());
151
+ if (currentCallId)
152
+ void this.media?.disconnect(currentCallId);
113
153
  this.current = {
154
+ callConnection: "idle",
114
155
  calls: new Map(),
115
156
  privateConversations: new Map(),
116
157
  transfers: new Map(),
@@ -121,6 +162,7 @@ export class SessionRuntime {
121
162
  this.cryptoHandles.clear();
122
163
  this.privateInvites.clear();
123
164
  this.interruptedPrivateConversations.clear();
165
+ this.watchedCommunityCallIds.clear();
124
166
  for (const transferId of this.connectingTransfers.keys())
125
167
  void this.transferAdapter?.disconnect(transferId);
126
168
  this.connectingTransfers.clear();
@@ -131,9 +173,12 @@ export class SessionRuntime {
131
173
  receiveStatus(status) {
132
174
  if (status.state !== "offline")
133
175
  return;
176
+ const currentCallId = this.current.currentCallId;
177
+ this.mediaGeneration += 1;
178
+ if (currentCallId)
179
+ void this.media?.disconnect(currentCallId);
180
+ this.mediaControls?.resetSession();
134
181
  const interrupted = [...this.current.privateConversations.values()].filter((session) => session.state !== "ended");
135
- if (!interrupted.length)
136
- return;
137
182
  for (const session of interrupted)
138
183
  this.interruptedPrivateConversations.add(session.id);
139
184
  for (const handle of this.cryptoHandles.values())
@@ -145,53 +190,61 @@ export class SessionRuntime {
145
190
  this.connectingTransfers.clear();
146
191
  this.transferKeys.clear();
147
192
  this.transferProgress.clear();
193
+ this.watchedCommunityCallIds.clear();
148
194
  this.set({
149
195
  ...this.current,
196
+ currentCallId: undefined,
197
+ callConnection: "idle",
198
+ calls: new Map(),
150
199
  privateConversations: new Map([...this.current.privateConversations].filter(([, session]) => session.state === "ended")),
151
200
  transfers: new Map(),
152
201
  privateMessages: new Map(),
153
202
  });
154
203
  }
155
204
  acceptCall(callId) {
205
+ return this.enqueue(() => this.acceptCallNow(callId));
206
+ }
207
+ beginCall(...args) {
156
208
  return this.enqueue(async () => {
157
- const activePrivate = [
158
- ...this.current.privateConversations.values(),
159
- ].find((item) => item.state !== "ended");
160
- if (activePrivate)
161
- await this.privateConversations.end(activePrivate.id);
162
- await this.calls.join(callId);
163
- if (!this.media)
164
- return;
165
- const call = this.current.calls.get(callId);
166
- if (!call)
167
- throw new Error("The call is not available");
168
- try {
169
- const admission = await this.calls.admit(callId);
170
- await this.media.connect({
171
- endpoint: this.endpoint,
172
- localUserId: this.ownUserId,
173
- call,
174
- admission,
175
- });
176
- await this.calls.mediaConnected(callId);
177
- }
178
- catch (reason) {
179
- await this.media.disconnect(callId).catch(() => undefined);
180
- await this.calls.leave(callId).catch(() => undefined);
181
- throw reason;
209
+ if (this.callView().current)
210
+ throw new Error("End the current call before starting another");
211
+ const existing = this.findJoinableTarget(args[0]);
212
+ if (existing) {
213
+ await this.acceptCallNow(existing.id);
214
+ return existing;
182
215
  }
216
+ const call = await this.calls.start(...args);
217
+ this.upsertCall(call);
218
+ this.selectCurrentCall(call.id);
219
+ this.setCallConnection("ringing", call);
220
+ await this.acceptCallNow(call.id);
221
+ return call;
183
222
  });
184
223
  }
224
+ declineCall(callId) {
225
+ return this.enqueue(() => this.calls.decline(callId));
226
+ }
185
227
  leaveCall(callId) {
186
228
  return this.enqueue(async () => {
229
+ this.mediaGeneration += 1;
230
+ this.setCallConnection("disconnecting");
187
231
  await this.media?.disconnect(callId);
188
232
  await this.calls.leave(callId);
233
+ if (this.current.currentCallId === callId)
234
+ this.clearCurrentCall();
235
+ this.mediaControls?.resetSession();
189
236
  });
190
237
  }
191
- async startCall(...args) {
192
- const call = await this.calls.start(...args);
193
- this.upsert("calls", call, (item) => item.id);
194
- return call;
238
+ startCall(...args) {
239
+ return this.enqueue(async () => {
240
+ if (this.callView().current)
241
+ throw new Error("End the current call before starting another");
242
+ const call = await this.calls.start(...args);
243
+ this.upsertCall(call);
244
+ this.selectCurrentCall(call.id);
245
+ this.setCallConnection("ringing", call);
246
+ return call;
247
+ });
195
248
  }
196
249
  async startPrivateConversation(peerId) {
197
250
  if (!this.crypto)
@@ -264,9 +317,11 @@ export class SessionRuntime {
264
317
  }
265
318
  acceptPrivateConversation(id, encryptedAnswer) {
266
319
  return this.enqueue(async () => {
267
- const activeCall = [...this.current.calls.values()].find((item) => !["ended", "declined", "expired"].includes(item.state));
320
+ const activeCall = this.callView().current;
268
321
  if (activeCall)
269
322
  await this.calls.leave(activeCall.id);
323
+ if (activeCall)
324
+ this.clearCurrentCall();
270
325
  let answer = encryptedAnswer;
271
326
  if (this.crypto) {
272
327
  const session = this.current.privateConversations.get(id);
@@ -319,7 +374,7 @@ export class SessionRuntime {
319
374
  }
320
375
  receiveRealtime(opcode, payload, timestamp) {
321
376
  if (opcode === 400)
322
- this.upsert("calls", decodeCall(payload), (item) => item.id);
377
+ this.upsertCall(decodeCall(payload));
323
378
  else if (opcode === 401)
324
379
  this.updateCallParticipant(payload);
325
380
  else if (opcode === 420)
@@ -444,6 +499,16 @@ export class SessionRuntime {
444
499
  this.set({ ...this.current, privateMessages });
445
500
  }
446
501
  receiveControl(frame) {
502
+ if (frame.opcode === 45) {
503
+ this.replaceCommunityCalls(frame.payload);
504
+ return;
505
+ }
506
+ if (frame.opcode === 46) {
507
+ const call = decodeCall(required(mapValue(frame.payload), 1));
508
+ this.watchedCommunityCallIds.add(call.id);
509
+ this.upsertCall(call);
510
+ return;
511
+ }
447
512
  if ([400, 401, 420, 422].includes(frame.opcode)) {
448
513
  this.receiveRealtime(frame.opcode, frame.payload, new Date());
449
514
  return;
@@ -469,12 +534,25 @@ export class SessionRuntime {
469
534
  if (conversation.encryptedInvitation)
470
535
  this.privateInvites.set(conversation.id, conversation.encryptedInvitation);
471
536
  }
537
+ const calls = keyed(arrayValue(required(map, 2)).map(decodeCall), (item) => item.id);
538
+ const previousCalls = this.current.calls;
539
+ const previousCurrent = this.callView().current;
540
+ const currentCallId = selectCurrentCallId(calls, this.ownUserId, this.current.currentCallId);
472
541
  this.set({
473
- calls: keyed(arrayValue(required(map, 2)).map(decodeCall), (item) => item.id),
542
+ callConnection: currentCallId ? this.current.callConnection : "idle",
543
+ currentCallId,
544
+ calls,
474
545
  privateConversations: keyed(availablePrivateConversations, (item) => item.id),
475
546
  transfers: this.current.transfers,
476
547
  privateMessages: this.current.privateMessages,
477
548
  });
549
+ for (const call of calls.values())
550
+ this.emitCallUpdate(call, previousCalls.get(call.id));
551
+ this.options.onCallUsers?.([...calls.values()].flatMap((call) => call.participants.map((item) => item.userId)));
552
+ const nextCurrent = this.callView().current;
553
+ this.emitCurrentChanged(previousCurrent, nextCurrent);
554
+ if (previousCurrent && !nextCurrent)
555
+ this.teardownLocalMedia(previousCurrent.id);
478
556
  }
479
557
  updateCallParticipant(payload) {
480
558
  const map = mapValue(payload);
@@ -498,7 +576,16 @@ export class SessionRuntime {
498
576
  ...call.participants.filter((item) => item.userId !== userId),
499
577
  { userId, state },
500
578
  ];
501
- this.upsert("calls", { ...call, participants }, (item) => item.id);
579
+ const updated = { ...call, participants };
580
+ this.upsertCall(updated);
581
+ this.emitCallEvent({
582
+ type: "participant-state-changed",
583
+ call: updated,
584
+ userId,
585
+ state,
586
+ previousState: call.participants.find((item) => item.userId === userId)
587
+ ?.state,
588
+ });
502
589
  }
503
590
  connectTransfer(transfer) {
504
591
  if (!this.transferAdapter)
@@ -565,6 +652,146 @@ export class SessionRuntime {
565
652
  this.upsert("privateConversations", { ...session, state: "ended" }, (item) => item.id);
566
653
  });
567
654
  }
655
+ async acceptCallNow(callId) {
656
+ const activePrivate = [...this.current.privateConversations.values()].find((item) => item.state !== "ended");
657
+ if (activePrivate)
658
+ await this.privateConversations.end(activePrivate.id);
659
+ const previous = this.callView().current;
660
+ if (previous && previous.id !== callId) {
661
+ this.mediaGeneration += 1;
662
+ this.setCallConnection("replacing", previous);
663
+ await this.media?.disconnect(previous.id);
664
+ await this.calls.leave(previous.id);
665
+ }
666
+ const call = this.current.calls.get(callId);
667
+ if (!call)
668
+ throw new Error("The call is not available");
669
+ this.selectCurrentCall(callId);
670
+ this.setCallConnection("connecting", call);
671
+ try {
672
+ await this.calls.join(callId);
673
+ if (!this.media) {
674
+ this.setCallConnection("active", call);
675
+ return;
676
+ }
677
+ const admission = await this.calls.admit(callId);
678
+ await this.media.connect({
679
+ endpoint: this.endpoint,
680
+ localUserId: this.ownUserId,
681
+ call,
682
+ admission,
683
+ });
684
+ await this.calls.mediaConnected(callId);
685
+ this.setCallConnection("active", call);
686
+ }
687
+ catch (reason) {
688
+ this.mediaGeneration += 1;
689
+ await this.media?.disconnect(callId).catch(() => undefined);
690
+ if (call.target.kind !== "user" && staleCallError(reason)) {
691
+ this.discardCall(callId);
692
+ this.mediaControls?.resetSession();
693
+ return;
694
+ }
695
+ await this.calls.leave(callId).catch(() => undefined);
696
+ if (this.current.currentCallId === callId)
697
+ this.clearCurrentCall();
698
+ this.mediaControls?.resetSession();
699
+ throw reason;
700
+ }
701
+ }
702
+ findJoinableTarget(target) {
703
+ return [...this.callView().joinable.values()].find((call) => sameCallTarget(call.target, target));
704
+ }
705
+ discardCall(callId) {
706
+ const calls = new Map(this.current.calls);
707
+ const previous = calls.get(callId);
708
+ const wasCurrent = this.current.currentCallId === callId;
709
+ calls.delete(callId);
710
+ this.set({
711
+ ...this.current,
712
+ calls,
713
+ currentCallId: this.current.currentCallId === callId
714
+ ? undefined
715
+ : this.current.currentCallId,
716
+ callConnection: this.current.currentCallId === callId
717
+ ? "idle"
718
+ : this.current.callConnection,
719
+ });
720
+ if (previous && wasCurrent)
721
+ this.emitCurrentChanged(previous, undefined);
722
+ }
723
+ receiveMediaEvent(event) {
724
+ if (event.callId !== this.current.currentCallId)
725
+ return;
726
+ if (event.type === "room-ended") {
727
+ void this.terminateMediaCall(event.callId, "room-ended");
728
+ return;
729
+ }
730
+ if (event.type !== "disconnected")
731
+ return;
732
+ if (event.reason) {
733
+ void this.terminateMediaCall(event.callId, event.reason);
734
+ return;
735
+ }
736
+ const generation = ++this.mediaGeneration;
737
+ this.setCallConnection("reconnecting", this.current.calls.get(event.callId));
738
+ void this.reconnectMedia(event.callId, generation);
739
+ }
740
+ async reconnectMedia(callId, generation) {
741
+ const attempts = this.options.reconnectAttempts ?? 6;
742
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
743
+ if (!this.reconnectCurrent(callId, generation))
744
+ return;
745
+ if (attempt > 0)
746
+ await (this.options.reconnectDelay ?? reconnectDelay)(attempt);
747
+ if (!this.reconnectCurrent(callId, generation))
748
+ return;
749
+ const call = this.current.calls.get(callId);
750
+ if (!call || !this.media)
751
+ return;
752
+ try {
753
+ const admission = await this.calls.admit(callId);
754
+ if (!this.reconnectCurrent(callId, generation))
755
+ return;
756
+ await this.media.connect({
757
+ endpoint: this.endpoint,
758
+ localUserId: this.ownUserId,
759
+ call,
760
+ admission,
761
+ });
762
+ if (!this.reconnectCurrent(callId, generation)) {
763
+ await this.media.disconnect(callId).catch(() => undefined);
764
+ return;
765
+ }
766
+ await this.calls.mediaConnected(callId);
767
+ this.setCallConnection("active", call);
768
+ return;
769
+ }
770
+ catch {
771
+ await this.media.disconnect(callId).catch(() => undefined);
772
+ }
773
+ }
774
+ if (this.reconnectCurrent(callId, generation))
775
+ await this.terminateMediaCall(callId, "reconnect-failed");
776
+ }
777
+ reconnectCurrent(callId, generation) {
778
+ return (generation === this.mediaGeneration &&
779
+ this.current.currentCallId === callId);
780
+ }
781
+ async terminateMediaCall(callId, reason) {
782
+ if (this.current.currentCallId !== callId)
783
+ return;
784
+ this.mediaGeneration += 1;
785
+ this.setCallConnection("disconnecting", this.current.calls.get(callId), reason);
786
+ await this.media?.disconnect(callId).catch(() => undefined);
787
+ await this.calls.leave(callId).catch(() => undefined);
788
+ if (this.current.currentCallId === callId)
789
+ this.clearCurrentCall(reason);
790
+ this.mediaControls?.resetSession();
791
+ const message = terminalCallMessage(reason);
792
+ if (message)
793
+ this.options.onError?.(new Error(message));
794
+ }
568
795
  requireTransfer(id) {
569
796
  const transfer = this.current.transfers.get(id);
570
797
  if (!transfer)
@@ -588,6 +815,115 @@ export class SessionRuntime {
588
815
  values.set(key(value), value);
589
816
  this.set({ ...this.current, [name]: values });
590
817
  }
818
+ upsertCall(call) {
819
+ const previous = this.current.calls.get(call.id);
820
+ const previousCurrent = this.callView().current;
821
+ const calls = new Map(this.current.calls);
822
+ calls.set(call.id, call);
823
+ const currentCallId = selectCurrentCallId(calls, this.ownUserId, this.current.currentCallId);
824
+ this.set({ ...this.current, calls, currentCallId });
825
+ this.emitCallUpdate(call, previous);
826
+ const nextCurrent = this.callView().current;
827
+ this.emitCurrentChanged(previousCurrent, nextCurrent);
828
+ if (previousCurrent && !nextCurrent)
829
+ this.teardownLocalMedia(previousCurrent.id);
830
+ this.connectAnsweredDirectCall(call);
831
+ this.options.onCallUsers?.(call.participants.map((item) => item.userId));
832
+ }
833
+ replaceCommunityCalls(payload) {
834
+ const map = mapValue(payload);
835
+ const incoming = arrayValue(required(map, 1)).map(decodeCall);
836
+ const incomingIds = new Set(incoming.map((call) => call.id));
837
+ const calls = new Map(this.current.calls);
838
+ for (const callId of this.watchedCommunityCallIds) {
839
+ if (incomingIds.has(callId))
840
+ continue;
841
+ const call = calls.get(callId);
842
+ if (call && !locallySelectable(call, this.ownUserId))
843
+ calls.delete(callId);
844
+ }
845
+ for (const call of incoming)
846
+ calls.set(call.id, call);
847
+ this.watchedCommunityCallIds = incomingIds;
848
+ this.set({ ...this.current, calls });
849
+ this.options.onCallUsers?.(incoming.flatMap((call) => call.participants.map((item) => item.userId)));
850
+ }
851
+ connectAnsweredDirectCall(call) {
852
+ const answered = call.participants.some((participant) => participant.userId !== this.ownUserId && participant.state === "active");
853
+ if (call.target.kind !== "user" ||
854
+ call.originatorId !== this.ownUserId ||
855
+ this.current.currentCallId !== call.id ||
856
+ this.current.callConnection !== "ringing" ||
857
+ !answered ||
858
+ this.connectingAnsweredCalls.has(call.id))
859
+ return;
860
+ this.connectingAnsweredCalls.add(call.id);
861
+ void this.acceptCall(call.id).finally(() => {
862
+ this.connectingAnsweredCalls.delete(call.id);
863
+ });
864
+ }
865
+ selectCurrentCall(callId) {
866
+ const previous = this.callView().current;
867
+ const currentCallId = selectCurrentCallId(this.current.calls, this.ownUserId, callId);
868
+ if (currentCallId === this.current.currentCallId)
869
+ return;
870
+ this.set({ ...this.current, currentCallId });
871
+ this.emitCurrentChanged(previous, this.callView().current);
872
+ }
873
+ clearCurrentCall(reason) {
874
+ const previous = this.callView().current;
875
+ const previousState = this.current.callConnection;
876
+ if (!this.current.currentCallId)
877
+ return;
878
+ this.set({
879
+ ...this.current,
880
+ currentCallId: undefined,
881
+ callConnection: "idle",
882
+ });
883
+ this.mediaControls?.resetSession();
884
+ this.emitCurrentChanged(previous, undefined);
885
+ this.emitCallEvent({
886
+ type: "connection-state-changed",
887
+ state: "idle",
888
+ previousState,
889
+ call: previous,
890
+ reason,
891
+ });
892
+ }
893
+ setCallConnection(state, call = this.callView().current, reason) {
894
+ const previousState = this.current.callConnection;
895
+ if (state === previousState)
896
+ return;
897
+ this.set({ ...this.current, callConnection: state });
898
+ this.emitCallEvent({
899
+ type: "connection-state-changed",
900
+ state,
901
+ previousState,
902
+ call,
903
+ reason,
904
+ });
905
+ }
906
+ teardownLocalMedia(callId) {
907
+ this.mediaGeneration += 1;
908
+ void this.media?.disconnect(callId);
909
+ this.mediaControls?.resetSession();
910
+ this.setCallConnection("idle");
911
+ }
912
+ emitCallUpdate(call, previous) {
913
+ if (previous === call)
914
+ return;
915
+ this.emitCallEvent({ type: "call-updated", call, previous });
916
+ if (previous && openCall(previous) && !openCall(call))
917
+ this.emitCallEvent({ type: "call-ended", call });
918
+ }
919
+ emitCurrentChanged(previous, call) {
920
+ if (previous?.id === call?.id)
921
+ return;
922
+ this.emitCallEvent({ type: "current-changed", call, previous });
923
+ }
924
+ emitCallEvent(event) {
925
+ this.callEventListeners.forEach((listener) => listener(event));
926
+ }
591
927
  set(state) {
592
928
  this.current = state;
593
929
  this.listeners.forEach((listener) => listener());
@@ -601,7 +937,63 @@ export class SessionRuntime {
601
937
  function keyed(values, key) {
602
938
  return new Map(values.map((value) => [key(value), value]));
603
939
  }
940
+ function openCall(call) {
941
+ return !["ended", "declined", "expired"].includes(call.state);
942
+ }
943
+ function callPhase(call) {
944
+ if (call.target.kind === "user")
945
+ return call.state === "ringing" ? "proposal" : "session";
946
+ if (call.target.kind === "channel")
947
+ return "session";
948
+ return call.establishedAt ? "session" : "proposal";
949
+ }
950
+ function sameCallTarget(left, right) {
951
+ if (left.kind !== right.kind)
952
+ return false;
953
+ if (left.kind === "user" && right.kind === "user")
954
+ return left.userId === right.userId;
955
+ if (left.kind === "conversation" && right.kind === "conversation")
956
+ return left.conversationId === right.conversationId;
957
+ return (left.kind === "channel" &&
958
+ right.kind === "channel" &&
959
+ left.channelId === right.channelId);
960
+ }
961
+ function staleCallError(reason) {
962
+ return reason instanceof SbpProtocolError && [4, 5].includes(reason.code);
963
+ }
964
+ function ownParticipant(call, ownUserId) {
965
+ return call.participants.find((participant) => participant.userId === ownUserId);
966
+ }
967
+ function locallyParticipating(call, ownUserId) {
968
+ const state = ownParticipant(call, ownUserId)?.state;
969
+ return openCall(call) && (state === "joining" || state === "active");
970
+ }
971
+ function locallySelectable(call, ownUserId) {
972
+ const state = ownParticipant(call, ownUserId)?.state;
973
+ return (openCall(call) && Boolean(state && state !== "declined" && state !== "left"));
974
+ }
975
+ function selectCurrentCallId(calls, ownUserId, preferred) {
976
+ const preferredCall = preferred ? calls.get(preferred) : undefined;
977
+ if (preferredCall && locallySelectable(preferredCall, ownUserId))
978
+ return preferredCall.id;
979
+ return [...calls.values()].find((call) => locallyParticipating(call, ownUserId))?.id;
980
+ }
604
981
  function completeTransferMetadata(transfer) {
605
982
  return Boolean(transfer.filename && transfer.mediaType && transfer.sha256);
606
983
  }
984
+ function reconnectDelay(attempt) {
985
+ const delay = Math.min(8_000, 1_000 * 2 ** (attempt - 1));
986
+ return new Promise((resolve) => setTimeout(resolve, delay));
987
+ }
988
+ function terminalCallMessage(reason) {
989
+ if (reason === "authorization_revoked")
990
+ return "You no longer have access to this call.";
991
+ if (reason === "policy_violation")
992
+ return "The call was ended because of invalid media activity.";
993
+ if (reason === "replaced")
994
+ return "This call was opened in another Sideband window.";
995
+ if (reason === "reconnect-failed")
996
+ return "The call could not be reconnected.";
997
+ return undefined;
998
+ }
607
999
  //# sourceMappingURL=session-runtime.js.map