arnacon-webrtc-service 0.2.7 → 0.2.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/package.json +1 -1
- package/webRTCservice/modules/calls/inbound/InboundCallFlow.js +3 -10
- package/webRTCservice/modules/calls/poly/PolySessionRegistry.js +7 -16
- package/webRTCservice/modules/calls/poly/__tests__/PolySessionRegistry.test.js +20 -0
- package/webRTCservice/modules/calls/poly/__tests__/Scenarios.test.js +25 -0
- package/webRTCservice/modules/calls/poly/__tests__/WebRtcNegotiation.test.js +10 -0
- package/webRTCservice/modules/calls/poly/__tests__/WebRtcNegotiation.werift.integration.test.js +13 -0
- package/webRTCservice/modules/calls/poly/negotiation/WebRtcNegotiation.js +3 -10
- package/webRTCservice/modules/calls/webrtc/WebRtcOfferUseCase.js +7 -15
- package/webRTCservice/modules/participants/signaling/SignalingEnvelope.js +3 -10
- package/webRTCservice/modules/participants/signaling/SignalingPipeline.js +9 -30
- package/webRTCservice/modules/runtime/CallPairRef.js +46 -0
- package/webRTCservice/modules/runtime/CallPairResolver.js +60 -0
- package/webRTCservice/modules/runtime/SessionStore.js +11 -11
- package/webRTCservice/webRTCmanager.js +33 -26
package/package.json
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
const { buildOfferPayload, serializeNotifyPayload } = require("../../participants/signaling/SignalingEnvelope");
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const trimmed = identity.trim();
|
|
6
|
-
const atPos = trimmed.indexOf("@");
|
|
7
|
-
if (atPos > 0) return trimmed.slice(0, atPos);
|
|
8
|
-
const dotPos = trimmed.indexOf(".");
|
|
9
|
-
if (dotPos > 0) return trimmed.slice(0, dotPos);
|
|
10
|
-
return trimmed;
|
|
11
|
-
}
|
|
2
|
+
const {
|
|
3
|
+
identityLabel,
|
|
4
|
+
} = require("../../runtime/CallPairRef");
|
|
12
5
|
|
|
13
6
|
function createInboundCallFlow({
|
|
14
7
|
createSession,
|
|
@@ -4,22 +4,13 @@
|
|
|
4
4
|
// Replaces the old linkedSessionId / bridgedWith / pendingBridges pairing.
|
|
5
5
|
|
|
6
6
|
const { PolySession } = require("./PolySession");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const trimmed = identity.trim();
|
|
12
|
-
const atPos = trimmed.indexOf("@");
|
|
13
|
-
if (atPos > 0) return trimmed.slice(0, atPos);
|
|
14
|
-
const dotPos = trimmed.indexOf(".");
|
|
15
|
-
if (dotPos > 0) return trimmed.slice(0, dotPos);
|
|
16
|
-
return trimmed;
|
|
17
|
-
}
|
|
7
|
+
const {
|
|
8
|
+
identityLabel,
|
|
9
|
+
pairKeyFromIdentities,
|
|
10
|
+
} = require("../../runtime/CallPairRef");
|
|
18
11
|
|
|
19
12
|
function pairKey(a, b) {
|
|
20
|
-
return
|
|
21
|
-
.sort()
|
|
22
|
-
.join("|");
|
|
13
|
+
return pairKeyFromIdentities(a, b);
|
|
23
14
|
}
|
|
24
15
|
|
|
25
16
|
class PolySessionRegistry {
|
|
@@ -47,7 +38,7 @@ class PolySessionRegistry {
|
|
|
47
38
|
}
|
|
48
39
|
|
|
49
40
|
keyForPair(a, b) {
|
|
50
|
-
return
|
|
41
|
+
return pairKeyFromIdentities(a, b);
|
|
51
42
|
}
|
|
52
43
|
|
|
53
44
|
// Resolve (or create) the PolySession for a pair and return which leg the
|
|
@@ -56,7 +47,7 @@ class PolySessionRegistry {
|
|
|
56
47
|
if (!a?.endpoint || !b?.endpoint) {
|
|
57
48
|
throw new Error("PolySessionRegistry.resolve requires both leg endpoints");
|
|
58
49
|
}
|
|
59
|
-
const key =
|
|
50
|
+
const key = pairKeyFromIdentities(a.endpoint, b.endpoint);
|
|
60
51
|
let poly = this.byKey.get(key);
|
|
61
52
|
if (!poly) {
|
|
62
53
|
poly = this._create(key, a, b);
|
|
@@ -68,6 +68,7 @@ test("different pair creates a different PolySession", () => {
|
|
|
68
68
|
|
|
69
69
|
test("pairKey is order-independent and label-normalized", () => {
|
|
70
70
|
assert.equal(pairKey("A.x.global", "b"), pairKey("b", "a"));
|
|
71
|
+
assert.equal(pairKey("Alice.SecNum.Global", "BOB"), pairKey("alice", "bob"));
|
|
71
72
|
});
|
|
72
73
|
|
|
73
74
|
test("destroy removes the PolySession and frees the key", async () => {
|
|
@@ -141,3 +142,22 @@ test("supports two simultaneous calls without cross-wiring state/media", async (
|
|
|
141
142
|
assert.equal(second.poly.legs.b.state, S.IN_CALL);
|
|
142
143
|
assert.equal(media.connects.length, 2, "media must bridge each call once");
|
|
143
144
|
});
|
|
145
|
+
|
|
146
|
+
test("same callee with different callers keeps separate pair records", () => {
|
|
147
|
+
const reg = buildRegistry();
|
|
148
|
+
const first = reg.resolve({
|
|
149
|
+
a: { endpoint: "alice.secnum.global", kind: "webrtc" },
|
|
150
|
+
b: { endpoint: "*9225", kind: "sip" },
|
|
151
|
+
target: "a",
|
|
152
|
+
});
|
|
153
|
+
const second = reg.resolve({
|
|
154
|
+
a: { endpoint: "carol.secnum.global", kind: "webrtc" },
|
|
155
|
+
b: { endpoint: "*9225", kind: "sip" },
|
|
156
|
+
target: "a",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
assert.notEqual(first.key, second.key);
|
|
160
|
+
assert.notEqual(first.poly, second.poly);
|
|
161
|
+
assert.equal(reg.get(first.key), first.poly);
|
|
162
|
+
assert.equal(reg.get(second.key), second.poly);
|
|
163
|
+
});
|
|
@@ -172,3 +172,28 @@ test("double-end race then immediate redial does not leak teardown state", async
|
|
|
172
172
|
await bAnswers(ctx, "answer-2");
|
|
173
173
|
expectInCall(ctx);
|
|
174
174
|
});
|
|
175
|
+
|
|
176
|
+
test("parallel calls that share a callee label stay isolated", async () => {
|
|
177
|
+
const first = buildWebRtcScenario();
|
|
178
|
+
const second = buildWebRtcScenario();
|
|
179
|
+
|
|
180
|
+
await transportOpenA(first);
|
|
181
|
+
await transportOpenB(first);
|
|
182
|
+
await transportOpenA(second);
|
|
183
|
+
await transportOpenB(second);
|
|
184
|
+
|
|
185
|
+
await aOffers(first, "offer-a");
|
|
186
|
+
await bAnswers(first, "answer-a");
|
|
187
|
+
await aOffers(second, "offer-c");
|
|
188
|
+
await bAnswers(second, "answer-c");
|
|
189
|
+
|
|
190
|
+
expectInCall(first);
|
|
191
|
+
expectInCall(second);
|
|
192
|
+
|
|
193
|
+
await aEnds(second);
|
|
194
|
+
await bCompletesEnd(second, "end-second");
|
|
195
|
+
|
|
196
|
+
// Ending one pair must not disturb the other active pair.
|
|
197
|
+
expectInCall(first);
|
|
198
|
+
expectEnded(second);
|
|
199
|
+
});
|
|
@@ -1015,3 +1015,13 @@ test("DEEP BUG REPRO: parser m-line failure realigns on data-only offer", async
|
|
|
1015
1015
|
);
|
|
1016
1016
|
assert.equal(pc.transceivers.find((t) => t.kind === "audio")?.sender?.track, null);
|
|
1017
1017
|
});
|
|
1018
|
+
|
|
1019
|
+
test("ring signaling keeps canonical caller label in payload", async () => {
|
|
1020
|
+
const { neg, signaling } = build();
|
|
1021
|
+
await neg.ring({ from: "alice.secnum.global" });
|
|
1022
|
+
const message = signaling.sent.at(-1);
|
|
1023
|
+
assert.equal(message?.msgType, "signaling");
|
|
1024
|
+
assert.equal(message?.payload?.type, "offer");
|
|
1025
|
+
assert.equal(message?.payload?.from, "alice");
|
|
1026
|
+
assert.equal(message?.payload?.to, "bob.secnum.global");
|
|
1027
|
+
});
|
package/webRTCservice/modules/calls/poly/__tests__/WebRtcNegotiation.werift.integration.test.js
CHANGED
|
@@ -253,3 +253,16 @@ test("WERIFT REPLAY: decline/end then redial with audio mid=2 stays reusable", a
|
|
|
253
253
|
await harness.dispose();
|
|
254
254
|
}
|
|
255
255
|
});
|
|
256
|
+
|
|
257
|
+
test("WERIFT REPLAY: ring payload keeps caller label normalized", async () => {
|
|
258
|
+
const harness = buildWeriftNegotiationHarness({ withAudioTrack: true, withDataChannel: true });
|
|
259
|
+
try {
|
|
260
|
+
await assert.doesNotReject(() => harness.neg.ring({ from: "alice.secnum.global" }));
|
|
261
|
+
const message = harness.signaling.sent.at(-1);
|
|
262
|
+
assert.equal(message?.msgType, "signaling");
|
|
263
|
+
assert.equal(message?.payload?.from, "alice");
|
|
264
|
+
assert.equal(message?.payload?.to, "bob.secnum.global");
|
|
265
|
+
} finally {
|
|
266
|
+
await harness.dispose();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
@@ -13,16 +13,9 @@ const {
|
|
|
13
13
|
alignEndCallAnswerSdp,
|
|
14
14
|
audioDirection,
|
|
15
15
|
} = require("./sdp");
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const trimmed = identity.trim();
|
|
20
|
-
const atPos = trimmed.indexOf("@");
|
|
21
|
-
if (atPos > 0) return trimmed.slice(0, atPos);
|
|
22
|
-
const dotPos = trimmed.indexOf(".");
|
|
23
|
-
if (dotPos > 0) return trimmed.slice(0, dotPos);
|
|
24
|
-
return trimmed;
|
|
25
|
-
}
|
|
16
|
+
const {
|
|
17
|
+
identityLabel,
|
|
18
|
+
} = require("../../../runtime/CallPairRef");
|
|
26
19
|
|
|
27
20
|
class WebRtcNegotiation extends CallNegotiationPort {
|
|
28
21
|
constructor({
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
const {
|
|
2
|
+
identityLabel,
|
|
3
|
+
normalizeSessionId,
|
|
4
|
+
pairKeyFromIdentities,
|
|
5
|
+
} = require("../../runtime/CallPairRef");
|
|
6
|
+
|
|
1
7
|
function createOfferFlow({
|
|
2
8
|
sessions,
|
|
3
9
|
sessionsByUser,
|
|
@@ -23,21 +29,6 @@ function createOfferFlow({
|
|
|
23
29
|
return addr;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
function identityLabel(identity) {
|
|
27
|
-
if (!identity || typeof identity !== "string") return identity;
|
|
28
|
-
const trimmed = identity.trim();
|
|
29
|
-
const atPos = trimmed.indexOf("@");
|
|
30
|
-
if (atPos > 0) return trimmed.slice(0, atPos);
|
|
31
|
-
const dotPos = trimmed.indexOf(".");
|
|
32
|
-
if (dotPos > 0) return trimmed.slice(0, dotPos);
|
|
33
|
-
return trimmed;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function normalizeSessionId(sessionId) {
|
|
37
|
-
if (!sessionId || typeof sessionId !== "string") return sessionId;
|
|
38
|
-
return sessionId.split("|").map(identityLabel).join("|");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
32
|
function relationSessionCandidates(from, to, sessionId) {
|
|
42
33
|
const fromLabel = identityLabel(from);
|
|
43
34
|
const toLabel = identityLabel(to);
|
|
@@ -47,6 +38,7 @@ function createOfferFlow({
|
|
|
47
38
|
candidates.add(`${fromLabel}|${toLabel}`);
|
|
48
39
|
candidates.add(`${toLabel}|${fromLabel}`);
|
|
49
40
|
candidates.add(stableKey(from, to));
|
|
41
|
+
candidates.add(pairKeyFromIdentities(from, to));
|
|
50
42
|
}
|
|
51
43
|
return candidates;
|
|
52
44
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const trimmed = identity.trim();
|
|
5
|
-
const atPos = trimmed.indexOf("@");
|
|
6
|
-
if (atPos > 0) return trimmed.slice(0, atPos);
|
|
7
|
-
const dotPos = trimmed.indexOf(".");
|
|
8
|
-
if (dotPos > 0) return trimmed.slice(0, dotPos);
|
|
9
|
-
return trimmed;
|
|
10
|
-
}
|
|
1
|
+
const {
|
|
2
|
+
identityLabel,
|
|
3
|
+
} = require("../../runtime/CallPairRef");
|
|
11
4
|
|
|
12
5
|
// The single source of truth for the wire sessionId we hand a client. Every
|
|
13
6
|
// client keys a session as sort(ownFullEns, peerBareNumber): it stores ITSELF as
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
function authNormalizeSessionId(sessionId) {
|
|
11
|
-
if (!sessionId || typeof sessionId !== "string") return sessionId;
|
|
12
|
-
if (!sessionId.includes("|")) return sessionId;
|
|
13
|
-
return sessionId.split("|").map(authIdentityLabel).join("|");
|
|
14
|
-
}
|
|
1
|
+
const {
|
|
2
|
+
identityLabel,
|
|
3
|
+
normalizeSessionId,
|
|
4
|
+
pairKeyFromIdentities,
|
|
5
|
+
} = require("../../runtime/CallPairRef");
|
|
6
|
+
|
|
7
|
+
const authIdentityLabel = identityLabel;
|
|
8
|
+
const authNormalizeSessionId = normalizeSessionId;
|
|
15
9
|
|
|
16
10
|
class SignalingAuthVerifier {
|
|
17
11
|
constructor({ blockchainGateway, sessions }) {
|
|
@@ -32,6 +26,7 @@ class SignalingAuthVerifier {
|
|
|
32
26
|
if (fromLabel && toLabel) {
|
|
33
27
|
candidates.add(`${fromLabel}|${toLabel}`);
|
|
34
28
|
candidates.add(`${toLabel}|${fromLabel}`);
|
|
29
|
+
candidates.add(pairKeyFromIdentities(fromLabel, toLabel));
|
|
35
30
|
}
|
|
36
31
|
for (const candidate of candidates) {
|
|
37
32
|
const session = this.sessions.get(candidate);
|
|
@@ -85,22 +80,6 @@ function createSignalingPipeline({
|
|
|
85
80
|
return null;
|
|
86
81
|
}
|
|
87
82
|
|
|
88
|
-
function identityLabel(identity) {
|
|
89
|
-
if (!identity || typeof identity !== "string") return identity;
|
|
90
|
-
const trimmed = identity.trim();
|
|
91
|
-
const atPos = trimmed.indexOf("@");
|
|
92
|
-
if (atPos > 0) return trimmed.slice(0, atPos);
|
|
93
|
-
const dotPos = trimmed.indexOf(".");
|
|
94
|
-
if (dotPos > 0) return trimmed.slice(0, dotPos);
|
|
95
|
-
return trimmed;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function normalizeSessionId(sessionId) {
|
|
99
|
-
if (!sessionId || typeof sessionId !== "string") return sessionId;
|
|
100
|
-
if (!sessionId.includes("|")) return sessionId;
|
|
101
|
-
return sessionId.split("|").map(identityLabel).join("|");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
83
|
function normalizeNotifyPayload(rawPayload) {
|
|
105
84
|
const source = rawPayload || {};
|
|
106
85
|
const nestedPayload = parseObjectPayload(source.payload);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function identityLabel(identity) {
|
|
2
|
+
if (!identity || typeof identity !== "string") return identity;
|
|
3
|
+
const trimmed = identity.trim();
|
|
4
|
+
const atPos = trimmed.indexOf("@");
|
|
5
|
+
if (atPos > 0) return trimmed.slice(0, atPos);
|
|
6
|
+
const dotPos = trimmed.indexOf(".");
|
|
7
|
+
if (dotPos > 0) return trimmed.slice(0, dotPos);
|
|
8
|
+
return trimmed;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeIdentityForKey(identity) {
|
|
12
|
+
return identityLabel(String(identity || "").toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createCallPairRef(caller, callee) {
|
|
16
|
+
return {
|
|
17
|
+
caller: caller || null,
|
|
18
|
+
callee: callee || null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pairKeyFromIdentities(a, b) {
|
|
23
|
+
return [normalizeIdentityForKey(a), normalizeIdentityForKey(b)]
|
|
24
|
+
.sort()
|
|
25
|
+
.join("|");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pairKeyFromRef(callPairRef) {
|
|
29
|
+
if (!callPairRef) return pairKeyFromIdentities("", "");
|
|
30
|
+
return pairKeyFromIdentities(callPairRef.caller, callPairRef.callee);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeSessionId(sessionId) {
|
|
34
|
+
if (!sessionId || typeof sessionId !== "string") return sessionId;
|
|
35
|
+
if (!sessionId.includes("|")) return sessionId;
|
|
36
|
+
return sessionId.split("|").map((part) => identityLabel(part)).join("|");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
identityLabel,
|
|
41
|
+
normalizeIdentityForKey,
|
|
42
|
+
createCallPairRef,
|
|
43
|
+
pairKeyFromIdentities,
|
|
44
|
+
pairKeyFromRef,
|
|
45
|
+
normalizeSessionId,
|
|
46
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createCallPairRef,
|
|
3
|
+
pairKeyFromRef,
|
|
4
|
+
pairKeyFromIdentities,
|
|
5
|
+
} = require("./CallPairRef");
|
|
6
|
+
|
|
7
|
+
class CallPairResolver {
|
|
8
|
+
constructor({ polyRegistry } = {}) {
|
|
9
|
+
if (!polyRegistry) throw new Error("CallPairResolver requires polyRegistry");
|
|
10
|
+
this.polyRegistry = polyRegistry;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fromSession(session) {
|
|
14
|
+
if (!session) return null;
|
|
15
|
+
if (session.callPairRef?.caller && session.callPairRef?.callee) return session.callPairRef;
|
|
16
|
+
return createCallPairRef(session.callerEns, session.toIdentity);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fromIdentities(caller, callee) {
|
|
20
|
+
return createCallPairRef(caller, callee);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
keyFromSession(session) {
|
|
24
|
+
const ref = this.fromSession(session);
|
|
25
|
+
if (!ref) return null;
|
|
26
|
+
return pairKeyFromRef(ref);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
keyFromIdentities(caller, callee) {
|
|
30
|
+
return pairKeyFromIdentities(caller, callee);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
keyFromPoly(poly) {
|
|
34
|
+
if (!poly) return null;
|
|
35
|
+
return pairKeyFromIdentities(poly.legs.a.endpoint, poly.legs.b.endpoint);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
polyForSession(session) {
|
|
39
|
+
const key = this.keyFromSession(session);
|
|
40
|
+
if (!key) return null;
|
|
41
|
+
return this.polyRegistry.get(key) || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
polyForOffer(offer) {
|
|
45
|
+
if (!offer) return null;
|
|
46
|
+
const key = this.keyFromIdentities(offer.from, offer.to);
|
|
47
|
+
if (!key) return null;
|
|
48
|
+
return this.polyRegistry.get(key) || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bindSessionPairRef(session, caller, callee) {
|
|
52
|
+
if (!session) return null;
|
|
53
|
+
session.callPairRef = createCallPairRef(caller, callee);
|
|
54
|
+
return session.callPairRef;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
CallPairResolver,
|
|
60
|
+
};
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
const {
|
|
2
|
+
identityLabel,
|
|
3
|
+
pairKeyFromIdentities,
|
|
4
|
+
normalizeSessionId,
|
|
5
|
+
createCallPairRef,
|
|
6
|
+
} = require("./CallPairRef");
|
|
7
|
+
|
|
1
8
|
class SessionStore {
|
|
2
9
|
constructor() {
|
|
3
10
|
this.sessions = new Map();
|
|
@@ -42,23 +49,15 @@ class SessionStore {
|
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
identityLabel(identity) {
|
|
45
|
-
|
|
46
|
-
const trimmed = identity.trim();
|
|
47
|
-
const atPos = trimmed.indexOf("@");
|
|
48
|
-
if (atPos > 0) return trimmed.slice(0, atPos);
|
|
49
|
-
const dotPos = trimmed.indexOf(".");
|
|
50
|
-
if (dotPos > 0) return trimmed.slice(0, dotPos);
|
|
51
|
-
return trimmed;
|
|
52
|
+
return identityLabel(identity);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
normalizeSessionId(sessionId) {
|
|
55
|
-
|
|
56
|
-
if (!sessionId.includes("|")) return sessionId;
|
|
57
|
-
return sessionId.split("|").map((part) => this.identityLabel(part)).join("|");
|
|
56
|
+
return normalizeSessionId(sessionId);
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
stableKey(a, b) {
|
|
61
|
-
return
|
|
60
|
+
return pairKeyFromIdentities(a, b);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
createSession(sessionId, callerEns, toIdentity, logger = console) {
|
|
@@ -92,6 +91,7 @@ class SessionStore {
|
|
|
92
91
|
},
|
|
93
92
|
},
|
|
94
93
|
inboundRingSent: false,
|
|
94
|
+
callPairRef: createCallPairRef(callerEns, toIdentity),
|
|
95
95
|
};
|
|
96
96
|
this.sessions.set(canonicalSessionId, session);
|
|
97
97
|
if (callerEns && toIdentity) {
|
|
@@ -143,6 +143,11 @@ const { LEG_STATES, canBeRung, isActiveCall } = require("./modules/calls/poly/st
|
|
|
143
143
|
const { isInactiveOffer } = require("./modules/calls/poly/negotiation/sdp");
|
|
144
144
|
const { routeToCodecPolicy } = require("./modules/media/negotiation/CodecPolicy");
|
|
145
145
|
const { narrowAudioOfferForCodecPolicy } = require("./modules/media/negotiation/SdpCodecNegotiator");
|
|
146
|
+
const {
|
|
147
|
+
identityLabel,
|
|
148
|
+
createCallPairRef,
|
|
149
|
+
} = require("./modules/runtime/CallPairRef");
|
|
150
|
+
const { CallPairResolver } = require("./modules/runtime/CallPairResolver");
|
|
146
151
|
const {
|
|
147
152
|
MediaStreamTrack,
|
|
148
153
|
} = werift;
|
|
@@ -720,6 +725,7 @@ const polyCore = createPolyCore({
|
|
|
720
725
|
});
|
|
721
726
|
const polyRegistry = polyCore.registry;
|
|
722
727
|
const polyIngress = polyCore.ingress;
|
|
728
|
+
const callPairResolver = new CallPairResolver({ polyRegistry });
|
|
723
729
|
console.log("[poly] PolySession core is the live coordinator");
|
|
724
730
|
|
|
725
731
|
|
|
@@ -824,6 +830,7 @@ async function handleInboundCallRequest(data, serviceContext = null) {
|
|
|
824
830
|
// An inbound call always brings a fresh session + FCM wake, so it
|
|
825
831
|
// must own a fresh poly -> tear any stale one down first.
|
|
826
832
|
await polyRegistry.destroy(polyRegistry.keyForPair(callerNumber, calleeEns), "inbound-fresh-call");
|
|
833
|
+
callPairResolver.bindSessionPairRef(session, callerNumber, calleeEns);
|
|
827
834
|
const { poly } = polyRegistry.resolve({
|
|
828
835
|
a: { endpoint: callerNumber, kind: "sip", phoneNumber, session },
|
|
829
836
|
b: {
|
|
@@ -963,20 +970,11 @@ function createPeerConnection(...args) {
|
|
|
963
970
|
// ─── PolySession ingress helpers (manager seam) ─────────────────────────────
|
|
964
971
|
|
|
965
972
|
function pLabel(identity) {
|
|
966
|
-
|
|
967
|
-
const t = identity.trim();
|
|
968
|
-
const at = t.indexOf("@");
|
|
969
|
-
if (at > 0) return t.slice(0, at);
|
|
970
|
-
const dot = t.indexOf(".");
|
|
971
|
-
if (dot > 0) return t.slice(0, dot);
|
|
972
|
-
return t;
|
|
973
|
+
return identityLabel(identity);
|
|
973
974
|
}
|
|
974
975
|
|
|
975
976
|
function polyForSession(session) {
|
|
976
|
-
|
|
977
|
-
return polyRegistry.getByEndpoint(session.callerEns)
|
|
978
|
-
|| polyRegistry.getByEndpoint(session.toIdentity)
|
|
979
|
-
|| null;
|
|
977
|
+
return callPairResolver.polyForSession(session);
|
|
980
978
|
}
|
|
981
979
|
|
|
982
980
|
// Which leg ref a webrtc message on `session` targets. callee-webrtc role => the
|
|
@@ -1088,6 +1086,7 @@ async function onDcRing(callerSessionId, payload) {
|
|
|
1088
1086
|
};
|
|
1089
1087
|
}
|
|
1090
1088
|
|
|
1089
|
+
callPairResolver.bindSessionPairRef(session, a.endpoint, b.endpoint);
|
|
1091
1090
|
const { poly } = polyRegistry.resolve({ a, b, target: "a" });
|
|
1092
1091
|
// Caller transport is already up (HTTP handshake). The SIP leg has no transport
|
|
1093
1092
|
// to negotiate (openOutbound happens on ring), so mark it usable too. A webrtc
|
|
@@ -1183,8 +1182,9 @@ function onDataChannelMessage(sessionId, rawMessage, meta = {}) {
|
|
|
1183
1182
|
if (!existing || !polyOwnsSession(existing, session, channelRole)) {
|
|
1184
1183
|
(async () => {
|
|
1185
1184
|
if (existing) {
|
|
1185
|
+
const existingKey = callPairResolver.keyFromPoly(existing);
|
|
1186
1186
|
await polyRegistry.destroy(
|
|
1187
|
-
|
|
1187
|
+
existingKey,
|
|
1188
1188
|
"new-ring-reset",
|
|
1189
1189
|
);
|
|
1190
1190
|
}
|
|
@@ -1221,18 +1221,21 @@ function onDataChannelMessage(sessionId, rawMessage, meta = {}) {
|
|
|
1221
1221
|
poly.onIngress(ref, event).catch((err) => {
|
|
1222
1222
|
if (action === "offer" && isRecoverableOfferIngressError(err) && !payload.__polyIngressRetriedOnce) {
|
|
1223
1223
|
payload.__polyIngressRetriedOnce = true;
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1224
|
+
// Keep the current poly/session/PC alive and retry ingress in place.
|
|
1225
|
+
// This mirrors client-side reuse semantics after decline/end flows.
|
|
1226
|
+
const latestSession = sessions.get(sessionId);
|
|
1227
|
+
const latestPoly = latestSession ? polyForSession(latestSession) : null;
|
|
1228
|
+
if (!latestSession || !latestPoly) {
|
|
1229
|
+
console.error(`[${sessionId}] poly ingress (${action}) retry skipped: no active session/poly`);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const retryRef = polyWebrtcRef(latestPoly, latestSession, channelRole);
|
|
1233
|
+
const retryEvent = polyIngress.toLegEvent(action, payload, { channelRole });
|
|
1234
|
+
if (!retryEvent) {
|
|
1235
|
+
console.error(`[${sessionId}] poly ingress (${action}) retry skipped: no retry event`);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
latestPoly.onIngress(retryRef, retryEvent).catch((retryErr) => {
|
|
1236
1239
|
console.error(`[${sessionId}] poly ingress (${action}) retry failed: ${retryErr.message}`);
|
|
1237
1240
|
});
|
|
1238
1241
|
return;
|
|
@@ -1243,7 +1246,7 @@ function onDataChannelMessage(sessionId, rawMessage, meta = {}) {
|
|
|
1243
1246
|
|
|
1244
1247
|
// HTTP /notify "answer" (callee picked up: secnum<->secnum leg or inbound callee).
|
|
1245
1248
|
async function onVerifiedNotifyAnswer(sessionId, offer, session) {
|
|
1246
|
-
const poly = polyForSession(session) ||
|
|
1249
|
+
const poly = polyForSession(session) || callPairResolver.polyForOffer(offer);
|
|
1247
1250
|
if (!poly) return { handled: false };
|
|
1248
1251
|
const ref = polyRefByEndpoint(poly, offer.from)
|
|
1249
1252
|
|| (poly.legs.b.kind === "webrtc" ? "b" : "a");
|
|
@@ -1340,7 +1343,10 @@ async function onTransportClosed(sessionId, event = {}) {
|
|
|
1340
1343
|
} catch (err) {
|
|
1341
1344
|
console.error(`[${sessionId}] poly transport-close failed: ${err.message}`);
|
|
1342
1345
|
}
|
|
1343
|
-
try {
|
|
1346
|
+
try {
|
|
1347
|
+
const key = callPairResolver.keyFromPoly(poly);
|
|
1348
|
+
await polyRegistry.destroy(key, event.reason || "transport-closed");
|
|
1349
|
+
} catch (_) {}
|
|
1344
1350
|
}
|
|
1345
1351
|
destroySession(sessionId, event.notify === true);
|
|
1346
1352
|
}
|
|
@@ -1386,6 +1392,7 @@ function sendDataChannelMessage(sessionId, message) {
|
|
|
1386
1392
|
|
|
1387
1393
|
function createSession(sessionId, callerEns, toIdentity) {
|
|
1388
1394
|
const session = sessionStore.createSession(sessionId, callerEns, toIdentity, console);
|
|
1395
|
+
session.callPairRef = createCallPairRef(callerEns, toIdentity);
|
|
1389
1396
|
const call = callFactory.fromSession(session);
|
|
1390
1397
|
session.call = call;
|
|
1391
1398
|
session.callId = call.id;
|