feat: improve feedback and App lifecycle reliability (#404)
Waiting feedback and tooltips now share consistent presentation lifecycles, while App shutdown and native-media operation failures stay within their owning session or output. This phase also consolidates the project standards and publishes searchable English/Chinese reader documentation alongside the homepage. Validation: repository hygiene and Markdown links; TypeScript, 971 Web tests and production build; website/documentation build; Go tests, vet and Windows/Linux builds with Windows capture checks; desktop/mobile UI and documentation review. Linux race, container and three-platform packaging checks run in the release pipeline. Physical macOS/Linux capture and broader network coverage remain outside this acceptance. The owner approved v1.4.0 publication. Existing empty-source, Windows URL-handler and public-invitation timeout reports remain tracked; this release makes no claim to resolve those device/network reports. The Windows capture-border request is next-phase work. ## Release notes ### 中文 这是一次以稳定性和交互一致性为重点的功能更新。等待与提示更有反馈,App 启动、原生分享和连接恢复的边界处理得到改善,使用与部署指南也有了统一的文档中心。 #### 新增与改进 - 欢迎语、等待短句和六类可轮换的标签页标题共用一致的显示规则。等待短句从一开始就出现,并在停留时轮换;明确的错误与操作提示保持固定。 - 房间准入等漫画采用更克制的小人物与循环动效。改进 tooltip 的定位与指向,减少对播放器的遮挡;状态、图标和文案保持相同含义。 - 新增中英文文档中心,提供搜索、手机目录、使用教程、问题排查与自部署指南。官网 FAQ 聚焦开始使用前的问题,详细排查统一进入文档中心。 #### 修复 - 使用 `--local` 启动不再清空已保存的 Piik 站点地址。 - 打开系统浏览器不再阻塞 App 的退出流程;改进临时公网邀请、终端和诊断导出在关闭时的清理及错误反馈。 - 单条原生媒体连接的操作失败不再被误判为整个会话的协议错误;重复停止分享保持幂等。 - 修复连接替换、转发停止和异常退出时的部分资源清理问题,避免迟到结果影响当前连接。 - Windows 原生采集中,单个输出的编码错误只结束对应输出;共享采集源本身的错误仍会结束分享。 #### 升级与已知边界 本次保持现有协议版本与配置格式。更新 App 或自托管 Server 后,重新打开页面即可使用对应的新界面。 个别设备的空窗口列表、Windows 浏览器启动异常,以及 Linux 编码输出的故障隔离仍在跟进。macOS/Linux 的物理采集和更广泛的公网网络场景仍需实机验证;本次不宣称这些问题已经解决,也不承诺所有网络都能直连。 ### English This feature update focuses on stability and consistent feedback. Waiting states are easier to follow, App startup and native sharing handle several failure cases more reliably, and the new documentation center brings usage and deployment guides together. #### New and improved - Welcome lines, waiting captions and six groups of rotating tab titles follow one presentation lifecycle. Waiting captions appear immediately and rotate while visible; actionable errors and instructions remain fixed. - Room-admission comics use restrained characters and looping motion. Tooltips point more accurately to their controls and interfere less with the player, with consistent meanings across illustrations, icons and copy. - A new English/Chinese documentation center includes search, mobile navigation, usage guides, troubleshooting and self-hosting. The homepage FAQ covers questions before getting started and links to the documentation for detailed help. #### Fixes - Starting with `--local` no longer clears a saved Piik site address. - Opening the system browser no longer blocks App shutdown. Temporary public invitations, the terminal and diagnostic exports handle cleanup and exit errors more consistently. - An operation failure on one native media connection no longer becomes a protocol failure for the entire session. Repeated stop-sharing requests are safe. - Fixed cleanup cases during connection replacement, relay shutdown and unexpected exits, preventing late results from affecting the current connection. - On Windows, an encoder failure for one native output stops that output only. A failure of the shared capture source still ends sharing. #### Updating and known limits Protocol versions and configuration formats are unchanged. After updating the App or your self-hosted Server, reopen the page to use its updated interface. Empty source lists on some devices, Windows browser-launch failures and Linux output-failure isolation remain under investigation. Physical macOS/Linux capture and broader public-network coverage still need device testing; this update does not claim to resolve those cases or guarantee a direct connection on every network.
This commit is contained in:
236 files changed
+8834
-2449
No files matched your search
+15
-20
@@ -39,7 +39,7 @@ import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// Node HTTP server settings of app.ts, mapped in map section 3.3 / D11.
|
||||
// HTTP request, header and keep-alive deadlines.
|
||||
const (
|
||||
requestTimeout = 10 * time.Second // httpServer.requestTimeout
|
||||
headersTimeout = 15 * time.Second // httpServer.headersTimeout
|
||||
@@ -55,7 +55,7 @@ type Options struct {
|
||||
Listener *net.TCPListener
|
||||
// Assets is the built Browser UI. A nil FS serves the API only, which is
|
||||
// the TS frontend mode "none"; webassets.FS() returns nil the same way
|
||||
// when no build was embedded (D3).
|
||||
// when no build was embedded.
|
||||
Assets fs.FS
|
||||
// Now returns Unix milliseconds; nil uses the wall clock.
|
||||
Now func() int64
|
||||
@@ -69,7 +69,7 @@ type Options struct {
|
||||
MaxUnauthenticatedSignalConnections int
|
||||
SiteAccessTTLSeconds int
|
||||
|
||||
// AfterFunc is the signaling timer factory (D5); nil uses time.AfterFunc.
|
||||
// AfterFunc is the signaling timer factory; nil uses time.AfterFunc.
|
||||
AfterFunc func(time.Duration, func()) func() bool
|
||||
// RoomStore replaces the store this package would build from Config.
|
||||
RoomStore *room.Store
|
||||
@@ -92,9 +92,8 @@ type Server struct {
|
||||
// STUN shares the same startup/shutdown owner, never the advertised ICE URLs.
|
||||
stunServer *stun.Server
|
||||
|
||||
// acceptingTraffic and signaling are the two closure variables app.ts
|
||||
// flipped together; both are read by request goroutines without a lock and
|
||||
// written under mu (hazard 13).
|
||||
// acceptingTraffic and signaling are read by request goroutines without
|
||||
// a lock and written together under mu.
|
||||
acceptingTraffic atomic.Bool
|
||||
signaling atomic.Pointer[signal.Server]
|
||||
|
||||
@@ -102,7 +101,7 @@ type Server struct {
|
||||
closing bool
|
||||
startupRequested bool
|
||||
// startupDone is closed when listen settles, which is what shutdown awaits
|
||||
// so the listener stays owned until an in-flight startup finishes (A6).
|
||||
// so the listener stays owned until an in-flight startup finishes.
|
||||
startupDone chan struct{}
|
||||
|
||||
shutdownOnce sync.Once
|
||||
@@ -221,9 +220,8 @@ func (s *Server) Listen(ctx context.Context) (int, error) {
|
||||
s.startupRequested = true
|
||||
s.mu.Unlock()
|
||||
|
||||
// Deferred: a rejected startupOperation still settled, so shutdown's wait
|
||||
// on it ended (app.ts:219). A panic escaping start() must likewise release
|
||||
// the latch, or every later Close blocks on it for good.
|
||||
// Release the startup latch even when start panics, so a later Close
|
||||
// cannot wait forever for startup to finish.
|
||||
defer close(s.startupDone)
|
||||
return s.start(ctx)
|
||||
}
|
||||
@@ -273,11 +271,8 @@ func (s *Server) start(ctx context.Context) (int, error) {
|
||||
})}
|
||||
}
|
||||
go func() {
|
||||
// bindHttpServer removed its "error" listener once the server was
|
||||
// listening (app.ts:315-333), so a later listener failure ended the
|
||||
// Node process. Serve only returns something other than
|
||||
// ErrServerClosed on a permanent accept failure, which nothing here
|
||||
// causes; report it rather than serving nothing in silence.
|
||||
// Report permanent accept failures so an unexpectedly stopped HTTP
|
||||
// listener is visible to the operator.
|
||||
if err := s.httpServer.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
|
||||
s.logger.Error("Piik HTTP server stopped unexpectedly", "errorType", fmt.Sprintf("%T", err))
|
||||
s.logger.Debug("http-listener-failed", diagnostics.Error(err))
|
||||
@@ -285,8 +280,8 @@ func (s *Server) start(ctx context.Context) (int, error) {
|
||||
}()
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
if err := s.reconcile(ctx); err != nil {
|
||||
// A13: give the database and the listener back before reporting.
|
||||
if err := s.reconcile(); err != nil {
|
||||
// Give the database and the listener back before reporting.
|
||||
var cleanup []error
|
||||
if closeErr := s.store.Close(); closeErr != nil {
|
||||
cleanup = append(cleanup, closeErr)
|
||||
@@ -330,7 +325,7 @@ func (s *Server) stopServing(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// reconcile restores room authority and enables traffic unless shutdown has begun.
|
||||
func (s *Server) reconcile(ctx context.Context) error {
|
||||
func (s *Server) reconcile() error {
|
||||
if err := s.store.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -371,14 +366,14 @@ func (s *Server) shutdown(ctx context.Context, endRooms bool) error {
|
||||
}
|
||||
|
||||
func (s *Server) runShutdown(ctx context.Context, endRooms bool) error {
|
||||
// O35: stop traffic, mark closing, then wait for an in-flight startup.
|
||||
// Stop traffic, mark closing, then wait for an in-flight startup.
|
||||
s.mu.Lock()
|
||||
s.acceptingTraffic.Store(false)
|
||||
s.closing = true
|
||||
awaitStartup := s.startupRequested
|
||||
s.mu.Unlock()
|
||||
if awaitStartup {
|
||||
// A6: the listen caller owns the startup error; shutdown only waits.
|
||||
// The listen caller owns the startup error; shutdown only waits.
|
||||
<-s.startupDone
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ type siteAccessBody struct {
|
||||
func sendJSON(writer http.ResponseWriter, status int, body any) {
|
||||
var buffer bytes.Buffer
|
||||
encoder := json.NewEncoder(&buffer)
|
||||
// D9: JSON.stringify does not escape HTML either.
|
||||
// JSON.stringify does not escape HTML either.
|
||||
encoder.SetEscapeHTML(false)
|
||||
if err := encoder.Encode(body); err != nil {
|
||||
panic(err)
|
||||
@@ -48,10 +48,8 @@ func sendJSON(writer http.ResponseWriter, status int, body any) {
|
||||
_, _ = writer.Write(encoded)
|
||||
}
|
||||
|
||||
// readJSONBody ports readJsonBody: the content type must be exactly
|
||||
// application/json once its parameters are cut, the body must be at most 1024
|
||||
// bytes and must not be empty. Every failure collapses to the route's 400, so
|
||||
// the messages only exist to keep the TS reasons readable.
|
||||
// readJSONBody requires application/json after removing parameters and a
|
||||
// nonempty body of at most 1024 bytes. Every failure maps to the route's 400.
|
||||
func readJSONBody(request *http.Request) ([]byte, error) {
|
||||
contentType, _, _ := strings.Cut(request.Header.Get("Content-Type"), ";")
|
||||
if strings.TrimSpace(contentType) != "application/json" {
|
||||
@@ -70,7 +68,7 @@ func readJSONBody(request *http.Request) ([]byte, error) {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// hasRequestBody ports hasRequestBody. net/http parses Content-Length and moves
|
||||
// net/http parses Content-Length and moves
|
||||
// Transfer-Encoding out of Header, so a chunked body is ContentLength -1 and an
|
||||
// unparseable length never reaches a handler at all.
|
||||
func hasRequestBody(request *http.Request) bool {
|
||||
@@ -96,7 +94,7 @@ func cookieHeader(request *http.Request) string {
|
||||
return strings.Join(request.Header.Values("Cookie"), "; ")
|
||||
}
|
||||
|
||||
// bearerToken ports readBearerToken: the prefix is case sensitive and an empty
|
||||
// bearerToken requires a case-sensitive prefix; an empty
|
||||
// token counts as absent.
|
||||
func bearerToken(request *http.Request) string {
|
||||
authorization := request.Header.Get("Authorization")
|
||||
|
||||
@@ -38,8 +38,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
}()
|
||||
|
||||
if !s.acceptingTraffic.Load() {
|
||||
// Hazard 13: no traffic at all until the SignalingServer exists, so
|
||||
// /healthz answers 503 while LiveKit reconciliation runs.
|
||||
// No traffic is accepted until startup completes; /healthz also returns 503.
|
||||
recorder.Header().Set("Cache-Control", "no-store")
|
||||
recorder.Header().Set("Retry-After", "1")
|
||||
sendJSON(recorder, http.StatusServiceUnavailable, errorBody{"Service starting"})
|
||||
@@ -152,7 +151,7 @@ func (s *Server) handleSiteAccess(writer http.ResponseWriter, request *http.Requ
|
||||
}
|
||||
if status.Required && status.Authenticated {
|
||||
// The Set-Cookie value is written verbatim, in the TS attribute
|
||||
// order (D11).
|
||||
// order.
|
||||
writer.Header().Set("Set-Cookie", access.createCookie())
|
||||
}
|
||||
sendJSON(writer, http.StatusOK, status)
|
||||
@@ -292,8 +291,8 @@ func (s *Server) handleRoomCreation(
|
||||
sendJSON(writer, http.StatusForbidden, errorBody{"Forbidden"})
|
||||
return
|
||||
}
|
||||
// Hazard 12: the cookie is the only room-creation credential. A
|
||||
// `Bearer <site password>` header must still be 401.
|
||||
// When site access requires a password, room creation uses its cookie;
|
||||
// a `Bearer <site password>` header does not authorize it.
|
||||
if !s.roomCreationAuthorized(request) {
|
||||
sendJSON(writer, http.StatusUnauthorized, errorBody{"Unauthorized"})
|
||||
return
|
||||
@@ -353,9 +352,8 @@ func (s *Server) authorizedRoomHostToken(
|
||||
return hostToken, true
|
||||
}
|
||||
|
||||
// allowedRequestOrigin ports isStrictlyAllowedRequestOrigin: the header must be
|
||||
// present, must parse, must be exactly its own serialised origin (so
|
||||
// "http://allowed.test/path" is refused) and must be configured.
|
||||
// allowedRequestOrigin requires a present, parseable header equal to its own
|
||||
// serialised origin (so "http://allowed.test/path" is refused) and configured.
|
||||
func (s *Server) allowedRequestOrigin(request *http.Request) bool {
|
||||
origin := request.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
@@ -369,12 +367,11 @@ func (s *Server) allowedRequestOrigin(request *http.Request) bool {
|
||||
return allowed
|
||||
}
|
||||
|
||||
// roomCreationAuthorized ports isRoomCreationAuthorized.
|
||||
func (s *Server) roomCreationAuthorized(request *http.Request) bool {
|
||||
return s.siteAccessForRequest(request).isAuthenticated(cookieHeader(request))
|
||||
}
|
||||
|
||||
// createRoomResponse of app.ts. signal.InviteURL is the one owner of the invite
|
||||
// signal.InviteURL is the one owner of the invite
|
||||
// URL shape; the rotated-grant notification on the signaling side uses it too.
|
||||
func (s *Server) createRoomResponse(created room.CreatedRoom) protocol.CreateRoomResponse {
|
||||
return protocol.CreateRoomResponse{
|
||||
|
||||
@@ -205,7 +205,6 @@ func Load(env map[string]string) (Config, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseEnvironment ports parseEnvironment for PIIK_ENV.
|
||||
func parseEnvironment(env map[string]string) (Environment, error) {
|
||||
value, present := env["PIIK_ENV"]
|
||||
if !present {
|
||||
@@ -218,7 +217,7 @@ func parseEnvironment(env map[string]string) (Environment, error) {
|
||||
return "", errors.New("PIIK_ENV must be development or production")
|
||||
}
|
||||
|
||||
// parseBoolean ports parseBoolean; every caller used a false fallback.
|
||||
// parseBoolean defaults an absent value to false.
|
||||
func parseBoolean(value string, name string) (bool, error) {
|
||||
switch value {
|
||||
case "", "false":
|
||||
@@ -229,7 +228,7 @@ func parseBoolean(value string, name string) (bool, error) {
|
||||
return false, fmt.Errorf("%s must be true or false", name)
|
||||
}
|
||||
|
||||
// parsePositiveInteger ports parsePositiveInteger, including Number.isSafeInteger.
|
||||
// parsePositiveInteger preserves Number.isSafeInteger's bounds.
|
||||
func parsePositiveInteger(value string, fallback int64, name string) (int64, error) {
|
||||
if value == "" {
|
||||
return fallback, nil
|
||||
@@ -241,7 +240,6 @@ func parsePositiveInteger(value string, fallback int64, name string) (int64, err
|
||||
return int64(number), nil
|
||||
}
|
||||
|
||||
// parseBoundedInteger ports parseBoundedInteger.
|
||||
func parseBoundedInteger(value string, fallback int64, name string, minimum, maximum int64) (int64, error) {
|
||||
parsed, err := parsePositiveInteger(value, fallback, name)
|
||||
if err != nil {
|
||||
@@ -299,7 +297,6 @@ func parseSFU(env map[string]string) (*SFUConfig, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseURLList ports parseUrlList.
|
||||
func parseURLList(value string, name string) ([]string, error) {
|
||||
if strings.TrimFunc(value, protocol.IsJSWhitespace) == "" {
|
||||
return nil, nil
|
||||
@@ -333,7 +330,6 @@ func parseStunURLList(value string, name string) ([]string, error) {
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// parseOrigins ports parseOrigins.
|
||||
func parseOrigins(value string, fallback string) (map[string]struct{}, error) {
|
||||
origins, err := parseURLList(value, "ALLOWED_ORIGINS")
|
||||
if err != nil {
|
||||
|
||||
@@ -275,6 +275,12 @@ func TestLoadAccepts(t *testing.T) {
|
||||
t.Errorf("STUNURLs = %v", c.STUNURLs)
|
||||
}
|
||||
}},
|
||||
{"ordinary STUN keeps an explicit non-default port", map[string]string{"STUN_URLS": "stun:stun.test:80"},
|
||||
func(t *testing.T, c Config) {
|
||||
if !slices.Equal(c.STUNURLs, []string{"stun:stun.test:80"}) || len(c.NATPredictionSTUNURLs) != 0 {
|
||||
t.Fatalf("unexpected ICE configuration: %+v", IceConfig(c))
|
||||
}
|
||||
}},
|
||||
{"NAT prediction with a base listener", map[string]string{
|
||||
"STUN_URLS": "stun:share.test:3478",
|
||||
"NAT_PREDICTION_ENABLED": "true",
|
||||
@@ -440,6 +446,9 @@ func TestLoadRejects(t *testing.T) {
|
||||
// NAT prediction.
|
||||
{"NAT prediction without STUN", map[string]string{"NAT_PREDICTION_ENABLED": "true"},
|
||||
"NAT_PREDICTION_ENABLED requires a STUN_URLS entry on UDP 3478"},
|
||||
{"NAT prediction cannot reinterpret explicit port 80", map[string]string{
|
||||
"STUN_URLS": "stun:share.test:80", "NAT_PREDICTION_ENABLED": "true",
|
||||
}, "NAT_PREDICTION_ENABLED requires a STUN_URLS entry on UDP 3478"},
|
||||
{"NAT prediction off the base port", map[string]string{
|
||||
"STUN_URLS": "stun:share.test:5349", "NAT_PREDICTION_ENABLED": "true",
|
||||
}, "NAT_PREDICTION_ENABLED requires a STUN_URLS entry on UDP 3478"},
|
||||
|
||||
@@ -35,7 +35,7 @@ func stunListenAddresses(host string, prediction bool) ([]string, error) {
|
||||
return listeners, nil
|
||||
}
|
||||
|
||||
// NATPredictionStunURLs ports natPredictionStunUrls: the two auxiliary
|
||||
// NATPredictionStunURLs returns the two auxiliary
|
||||
// listeners derived from the first ordinary STUN authority on UDP 3478.
|
||||
func NATPredictionStunURLs(stunURLs []string) []string {
|
||||
for _, stunURL := range stunURLs {
|
||||
@@ -45,12 +45,6 @@ func NATPredictionStunURLs(stunURLs []string) []string {
|
||||
continue
|
||||
}
|
||||
port := authority.Port()
|
||||
// The WHATWG parser drops a port equal to the scheme default, so
|
||||
// "stun:host:80" reaches the TypeScript's `: NAT_PREDICTION_BASE_PORT`
|
||||
// fallback and counts as a base listener.
|
||||
if port == defaultSchemePort("http") {
|
||||
port = ""
|
||||
}
|
||||
if port != "" {
|
||||
number, err := strconv.Atoi(port)
|
||||
if err != nil || number != natPredictionBasePort {
|
||||
@@ -72,7 +66,6 @@ func NATPredictionStunURLs(stunURLs []string) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IceConfig ports createIceConfig.
|
||||
func IceConfig(c Config) protocol.IceConfig {
|
||||
// A configured empty list (Local without prediction) is kept as-is; only an
|
||||
// absent one is derived, so nil and empty differ here.
|
||||
|
||||
@@ -104,16 +104,14 @@ func TestNATPredictionStunURLs(t *testing.T) {
|
||||
[]string{"stun:share.test:3479", "stun:share.test:3480"}},
|
||||
{"leading zeros in the port", []string{"stun:share.test:03478"},
|
||||
[]string{"stun:share.test:3479", "stun:share.test:3480"}},
|
||||
// WHATWG drops a port equal to the scheme default, so "http://host:80"
|
||||
// reports no port and the TS fallback treats it as 3478.
|
||||
{"HTTP default port reads as the base port", []string{"stun:share.test:80"},
|
||||
[]string{"stun:share.test:3479", "stun:share.test:3480"}},
|
||||
{"explicit port 80 is not the base port", []string{"stun:share.test:80"}, nil},
|
||||
{"leading zero port 80 is not the base port", []string{"stun:share.test:00080"}, nil},
|
||||
{"uppercase scheme and host", []string{"STUN:Share.TEST:3478"},
|
||||
[]string{"stun:share.test:3479", "stun:share.test:3480"}},
|
||||
{"IPv6 authority", []string{"stun:[2001:DB8::1]:3478"},
|
||||
[]string{"stun:[2001:db8::1]:3479", "stun:[2001:db8::1]:3480"}},
|
||||
{"first base listener wins",
|
||||
[]string{"stun:a.test:5349", "stun:b.test:3478", "stun:c.test:3478"},
|
||||
[]string{"stun:a.test:80", "stun:b.test:3478", "stun:c.test:3478"},
|
||||
[]string{"stun:b.test:3479", "stun:b.test:3480"}},
|
||||
{"no base listener", []string{"stun:a.test:5349", "stun:b.test:19302"}, nil},
|
||||
// TS: indexOf(":") is -1, so the whole text becomes the authority.
|
||||
|
||||
@@ -24,7 +24,6 @@ type LocalOptions struct {
|
||||
NATPredictionSTUNURLs []string
|
||||
}
|
||||
|
||||
// Local ports createLocalServerConfig.
|
||||
func Local(options LocalOptions) (Config, error) {
|
||||
port := options.Port
|
||||
if port == 0 {
|
||||
@@ -118,7 +117,7 @@ func allValidStunURLs(values []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// publicHTTPSOrigin ports publicHTTPSOrigin: every failure shares one message.
|
||||
// publicHTTPSOrigin uses one error message for all invalid origins.
|
||||
func publicHTTPSOrigin(value string) (*url.URL, error) {
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil || parsed.Scheme != "https" || parsed.Host == "" ||
|
||||
@@ -129,7 +128,7 @@ func publicHTTPSOrigin(value string) (*url.URL, error) {
|
||||
return &url.URL{Scheme: "https", Host: normalizedHost(parsed), Path: "/"}, nil
|
||||
}
|
||||
|
||||
// localIPv4 ports localIPv4. Node's isIP(value) === 4 accepts only a dotted
|
||||
// Node's isIP(value) === 4 accepts only a dotted
|
||||
// quad, so an IPv4-mapped IPv6 literal such as "::ffff:192.168.1.10" — which
|
||||
// net.ParseIP(...).To4() does resolve — is rejected here as well.
|
||||
func localIPv4(value string, name string) (string, error) {
|
||||
|
||||
@@ -197,9 +197,8 @@ func (SetSharingPausedMessage) isClientMessage() {}
|
||||
func (StopSharingMessage) isClientMessage() {}
|
||||
func (AbandonRoomMessage) isClientMessage() {}
|
||||
|
||||
// DecodeClientMessage ports decodeClientMessage: JSON.parse followed by
|
||||
// clientMessageSchema.parse. Every member of the union carries a distinct
|
||||
// `type` literal, so the union is resolved by that key.
|
||||
// DecodeClientMessage parses JSON and validates the message schema.
|
||||
// Every member carries a distinct `type` literal, so that key resolves the union.
|
||||
func DecodeClientMessage(data []byte) (ClientMessage, error) {
|
||||
messageType, err := typeOf(data, "type")
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// fields holds the raw members of one JSON object. Validation reads it to tell
|
||||
@@ -21,6 +23,22 @@ func decodeObject(data []byte, target any) (fields, error) {
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// These wire structs declare every member with a JSON tag. Go's struct
|
||||
// decoder also accepts case-folded names; reject those before they can
|
||||
// disagree with the exact presence/null checks below.
|
||||
targetType := reflect.TypeOf(target).Elem()
|
||||
known := make(map[string]struct{}, targetType.NumField())
|
||||
for index := range targetType.NumField() {
|
||||
name, _, _ := strings.Cut(targetType.Field(index).Tag.Get("json"), ",")
|
||||
if name != "" && name != "-" {
|
||||
known[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
for name := range raw {
|
||||
if _, ok := known[name]; !ok {
|
||||
return nil, fmt.Errorf("unknown field %q", name)
|
||||
}
|
||||
}
|
||||
// JSON.parse keeps only the last occurrence of a duplicate key, so the
|
||||
// earlier ones must never reach the strict decode. Re-encoding the member
|
||||
// map drops them.
|
||||
|
||||
@@ -35,6 +35,27 @@ func TestRuntimeCapabilities(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictMemberNamesCannotPopulateForbiddenOrOptionalFields(t *testing.T) {
|
||||
for _, sample := range []struct {
|
||||
name string
|
||||
data string
|
||||
target any
|
||||
}{
|
||||
{"none upstream with folded empty peer", `{"kind":"none","peerid":""}`, &MediaRouteUpstream{}},
|
||||
{"host diagnostic with folded zero ordinal", `{"kind":"host","Ordinal":0}`, &RouteDiagnosticParent{}},
|
||||
{"host presence with folded false ready", `{"role":"host","peerId":"host_12345678","displayName":"Host","upstream":{"kind":"none"},"mediaready":false}`, &ParticipantPresenceEntry{}},
|
||||
{"viewer presence with folded false ready", `{"role":"viewer","peerId":"viewer_12345678","displayName":"Viewer","upstream":{"kind":"none"},"mediaready":false}`, &ParticipantPresenceEntry{}},
|
||||
} {
|
||||
t.Run(sample.name, func(t *testing.T) {
|
||||
if err := json.Unmarshal([]byte(sample.data), sample.target); err == nil {
|
||||
t.Fatal("a noncanonical member must not populate a schema field")
|
||||
}
|
||||
})
|
||||
}
|
||||
assertServerMessage(t, "folded route policy cannot be ignored by its default",
|
||||
object(with(drop(authenticatedHost(), "routePolicy"), member{"routepolicy", testRoutePolicy})), false)
|
||||
}
|
||||
|
||||
// member is one JSON object key with its raw encoded value, so the tests can
|
||||
// reproduce the exact wire shapes from tests/protocol.test.ts (including key
|
||||
// presence, which a Go map cannot express).
|
||||
|
||||
@@ -24,8 +24,7 @@ const (
|
||||
var ErrEndpointMediaCopyCapacity = errors.New(
|
||||
"Endpoint media copy capacity must be 1, 2, or 3")
|
||||
|
||||
// EndpointMediaCopyLimit ports endpointMediaCopyLimit. It panics on an invalid
|
||||
// capacity, which is where the TypeScript throws.
|
||||
// EndpointMediaCopyLimit panics on an invalid capacity, where TypeScript throws.
|
||||
func EndpointMediaCopyLimit(capacity int, phase EndpointMediaCopyPhase) int {
|
||||
if err := AssertEndpointMediaCopyCapacity(capacity); err != nil {
|
||||
panic(err)
|
||||
@@ -36,20 +35,17 @@ func EndpointMediaCopyLimit(capacity int, phase EndpointMediaCopyPhase) int {
|
||||
return min(capacity+1, MaxEndpointMediaCopyCapacity)
|
||||
}
|
||||
|
||||
// EndpointMediaCopyCountFits ports endpointMediaCopyCountFits.
|
||||
func EndpointMediaCopyCountFits(count, capacity int, phase EndpointMediaCopyPhase) bool {
|
||||
return count >= 0 &&
|
||||
IsEndpointMediaCopyCapacity(capacity) &&
|
||||
count <= EndpointMediaCopyLimit(capacity, phase)
|
||||
}
|
||||
|
||||
// IsEndpointMediaCopyCapacity ports isEndpointMediaCopyCapacity. The
|
||||
// TypeScript Number.isSafeInteger guard is implicit in Go's int.
|
||||
// Go's int makes the fractional-value part of Number.isSafeInteger implicit.
|
||||
func IsEndpointMediaCopyCapacity(capacity int) bool {
|
||||
return capacity >= 1 && capacity <= MaxEndpointMediaCopyCapacity
|
||||
}
|
||||
|
||||
// AssertEndpointMediaCopyCapacity ports assertEndpointMediaCopyCapacity.
|
||||
func AssertEndpointMediaCopyCapacity(capacity int) error {
|
||||
if !IsEndpointMediaCopyCapacity(capacity) {
|
||||
return ErrEndpointMediaCopyCapacity
|
||||
@@ -76,8 +72,7 @@ var predictedCandidatePattern = regexp.MustCompile(
|
||||
`(?i)^candidate:s[pm][0-9]+[\t\n\v\f\r \x{00a0}\x{1680}\x{2000}-\x{200a}` +
|
||||
`\x{2028}\x{2029}\x{202f}\x{205f}\x{3000}\x{feff}]`)
|
||||
|
||||
// CandidateSignalOriginOf ports candidateSignalOrigin. An empty string stands
|
||||
// in for the TypeScript null (both yield "end").
|
||||
// CandidateSignalOriginOf treats an empty string as TypeScript null (both yield "end").
|
||||
func CandidateSignalOriginOf(candidate string) CandidateSignalOrigin {
|
||||
if strings.TrimFunc(candidate, IsJSWhitespace) == "" {
|
||||
return CandidateOriginEnd
|
||||
@@ -92,8 +87,8 @@ func CandidateSignalOriginOf(candidate string) CandidateSignalOrigin {
|
||||
// src/shared/packet-loss.ts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// PacketLossPercentFromDeltas ports packetLossPercentFromDeltas. A nil
|
||||
// argument is the TypeScript null; ok is false where it returns null.
|
||||
// PacketLossPercentFromDeltas maps nil arguments to TypeScript null;
|
||||
// ok is false where the shared calculation returns null.
|
||||
func PacketLossPercentFromDeltas(packetsReceivedDelta, packetsLostDelta *float64) (float64, bool) {
|
||||
if packetsReceivedDelta == nil || packetsLostDelta == nil ||
|
||||
*packetsReceivedDelta < 0 || *packetsLostDelta < 0 {
|
||||
@@ -156,9 +151,8 @@ var videoCodecParameterRules = map[string]codecParameterRule{
|
||||
|
||||
var videoMimeTypePattern = regexp.MustCompile(`(?i)^video/[A-Za-z0-9.+-]{1,32}$`)
|
||||
|
||||
// IsCanonicalVideoCodecEvidence ports isCanonicalVideoCodecEvidence. Empty
|
||||
// strings are the TypeScript nulls; the schema rejects a genuinely empty codec
|
||||
// string before this refine runs.
|
||||
// IsCanonicalVideoCodecEvidence uses empty strings for TypeScript nulls;
|
||||
// the schema rejects an empty codec string before this refinement runs.
|
||||
func IsCanonicalVideoCodecEvidence(codec, codecProfile, codecParameters string) bool {
|
||||
rule, ok := videoCodecParameterRules[strings.ToLower(codec)]
|
||||
if codec == "" || !ok {
|
||||
|
||||
@@ -83,8 +83,7 @@ var forbiddenDisplayNameCharacters = regexp.MustCompile(
|
||||
`[\p{Cc}\p{Zl}\p{Zp}\x{061c}\x{200b}\x{200e}\x{200f}\x{202a}-\x{202e}` +
|
||||
`\x{2060}\x{2066}-\x{2069}\x{feff}]`)
|
||||
|
||||
// NormalizeDisplayName ports normalizeDisplayName. The second result is false
|
||||
// where the TypeScript returns null.
|
||||
// NormalizeDisplayName returns false where the shared TypeScript normalizer returns null.
|
||||
func NormalizeDisplayName(value string) (string, bool) {
|
||||
if forbiddenDisplayNameCharacters.MatchString(value) {
|
||||
return "", false
|
||||
@@ -169,7 +168,12 @@ func ValidDisplayName(value string) bool {
|
||||
func hasUnpairedSurrogateEscape(token []byte) bool {
|
||||
text := string(token)
|
||||
for index := 0; index+6 <= len(text); index++ {
|
||||
if text[index] != '\\' || text[index+1] != 'u' {
|
||||
if text[index] != '\\' {
|
||||
continue
|
||||
}
|
||||
if text[index+1] != 'u' {
|
||||
// Consume the whole escape, including \\, before looking for \u.
|
||||
index++
|
||||
continue
|
||||
}
|
||||
unit, err := strconv.ParseUint(text[index+2:index+6], 16, 32)
|
||||
@@ -188,6 +192,8 @@ func hasUnpairedSurrogateEscape(token []byte) bool {
|
||||
index += 11
|
||||
case unit >= 0xdc00 && unit <= 0xdfff:
|
||||
return true
|
||||
default:
|
||||
index += 5
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -136,6 +136,8 @@ func TestValidStunURL(t *testing.T) {
|
||||
{"STUN:[2001:db8::1]:3478", true},
|
||||
{"stun:stun.test", true},
|
||||
{"stun:stun.test/path", false},
|
||||
{"stun:stun.test:80/", false},
|
||||
{"stun:stun.test:3478/", false},
|
||||
{"stun:stun.test?transport=udp", false},
|
||||
{"stun:stun.test#fragment", false},
|
||||
{"stun:", false},
|
||||
|
||||
@@ -222,9 +222,8 @@ func EncodeServerMessage(message ServerMessage) ([]byte, error) {
|
||||
return marshalJSON(message)
|
||||
}
|
||||
|
||||
// DecodeServerMessage ports decodeServerMessage. The server never reads its
|
||||
// own messages in production; this exists so the shared wire fixture can be
|
||||
// replayed from Go.
|
||||
// DecodeServerMessage supports replaying the shared wire fixture from Go.
|
||||
// The server never reads its own messages in production.
|
||||
func DecodeServerMessage(data []byte) (ServerMessage, error) {
|
||||
messageType, err := typeOf(data, "type")
|
||||
if err != nil {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
// The pure-Go driver: the port must build with CGO_ENABLED=0 (D2).
|
||||
// The pure-Go driver supports builds with CGO_ENABLED=0.
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
@@ -81,7 +81,7 @@ type StoredRoomAuthority struct {
|
||||
|
||||
// Database is RoomDatabase: the SQLite stable authority. It holds one
|
||||
// connection for its lifetime, because `PRAGMA locking_mode = EXCLUSIVE` is a
|
||||
// connection property that a recycled pool connection would silently drop (D2).
|
||||
// connection property that a recycled pool connection would silently drop.
|
||||
type Database struct {
|
||||
path string
|
||||
db *sql.DB
|
||||
|
||||
@@ -188,7 +188,7 @@ type Store struct {
|
||||
maxRooms int
|
||||
maxViewersPerRoom int
|
||||
// gate is per store; the TypeScript module-level gate was shared by every
|
||||
// RoomStore in a process, of which there was one (D11).
|
||||
// RoomStore in a process, of which there was one.
|
||||
gate gate
|
||||
}
|
||||
|
||||
@@ -916,7 +916,7 @@ func (s *Store) takeRoomCode(preferredRoomID string) (string, error) {
|
||||
// uniformIndex draws a uniform index below count from 8 random bytes, rejecting
|
||||
// the values above the largest multiple of count. The TypeScript computed the
|
||||
// limit in BigInt as 2^64 - 2^64 % count; (MaxUint64 % count + 1) % count is
|
||||
// the same remainder without the uint64 wrap at a power-of-two count (D11).
|
||||
// the same remainder without the uint64 wrap at a power-of-two count.
|
||||
func uniformIndex(count int, random func(size int) []byte) (int, error) {
|
||||
span := uint64(count)
|
||||
remainder := (math.MaxUint64%span + 1) % span
|
||||
|
||||
@@ -459,13 +459,10 @@ type Options struct {
|
||||
NatPredictionEnabled bool
|
||||
}
|
||||
|
||||
// Controller ports RoomRouteController. It never locks: the caller holds
|
||||
// signal.Server.mu around every call. The public methods live in the files
|
||||
// named after the TS helper clusters (controller.go, operation.go,
|
||||
// quality.go, ...); every signature below the types is unchanged.
|
||||
// Controller never locks: the caller holds signal.Server.mu around every call.
|
||||
type Controller struct {
|
||||
// hostPeerID is its own mutable field because RebindHostIdentity
|
||||
// reassigns it (TS: options.hostPeerId = peerId at line 660); the other
|
||||
// reassigns it; the other
|
||||
// options are copied once and never change.
|
||||
hostPeerID string
|
||||
debugRoomID string
|
||||
@@ -477,7 +474,7 @@ type Controller struct {
|
||||
natPredictionEnabled bool
|
||||
|
||||
// Insertion order is load-bearing for participants, upstreamByViewer and
|
||||
// directContinuations (map §4.1-4.3); the opportunity ledgers and the
|
||||
// directContinuations; the opportunity ledgers and the
|
||||
// retiring set are ordered for the same delete-while-iterating semantics.
|
||||
participants ordered.Map[string, *participant]
|
||||
upstreamByViewer ordered.Map[string, *CommittedEdge]
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
)
|
||||
|
||||
// buildCandidates ports 2767: the full plan list filtered by the
|
||||
// buildCandidates filters the full plan list by the
|
||||
// availability opportunity ledger.
|
||||
func (c *Controller) buildCandidates(childPeerID string, sfuOnly bool) []CandidatePlan {
|
||||
plans := c.buildCandidatePlans(childPeerID, sfuOnly, true)
|
||||
@@ -21,10 +21,10 @@ func (c *Controller) buildCandidates(childPeerID string, sfuOnly bool) []Candida
|
||||
return out
|
||||
}
|
||||
|
||||
// buildCandidatePlans ports 2777: eligible parents sorted by (untried
|
||||
// buildCandidatePlans sorts eligible parents by (untried
|
||||
// attempts when preferUntried) then depth, remaining capacity descending,
|
||||
// stablePairRank, compareParticipant; the SFU tuple is spliced into index 1
|
||||
// (§4.4 #28) and the list is deduped by tupleKey (§4.4 #29).
|
||||
// and the list is deduped by tupleKey.
|
||||
func (c *Controller) buildCandidatePlans(childPeerID string, sfuOnly, preferUntried bool) []CandidatePlan {
|
||||
descendants := c.descendantsOf(childPeerID)
|
||||
parents := []*participant{}
|
||||
@@ -98,11 +98,11 @@ func (c *Controller) buildCandidatePlans(childPeerID string, sfuOnly, preferUntr
|
||||
return plans
|
||||
}
|
||||
|
||||
// planCandidate ports 3947: decides none / overlap / bounded-gap from the
|
||||
// planCandidate decides none / overlap / bounded-gap from the
|
||||
// producer's copy budget; nil when the producer is gone or over budget.
|
||||
func (c *Controller) planCandidate(childPeerID string, tuple CandidateTuple) *CandidatePlan {
|
||||
if tuple.Kind == UpstreamSfu && tuple.Publication == PublicationReuse {
|
||||
// No producer on sfu/reuse (3949); endpointTransitionEquals compares it.
|
||||
// No producer on sfu/reuse; endpointTransitionEquals compares it.
|
||||
return &CandidatePlan{Tuple: tuple, EndpointTransition: EndpointTransition{Kind: TransitionNone}}
|
||||
}
|
||||
producerPeerID := c.hostPeerID
|
||||
@@ -128,7 +128,6 @@ func (c *Controller) planCandidate(childPeerID string, tuple CandidateTuple) *Ca
|
||||
return &CandidatePlan{Tuple: tuple, EndpointTransition: EndpointTransition{Kind: TransitionBoundedGap, ProducerPeerID: &producer0, Retire: retire}}
|
||||
}
|
||||
|
||||
// retirementFor ports 3969.
|
||||
func (c *Controller) retirementFor(childPeerID, producerPeerID string, tuple CandidateTuple) *EndpointRetirement {
|
||||
old, _ := c.upstreamByViewer.Get(childPeerID)
|
||||
if old != nil && old.Kind == UpstreamPeer && old.ParentPeerID == producerPeerID && old.PhysicalActive {
|
||||
@@ -159,7 +158,7 @@ func (c *Controller) retirementFor(childPeerID, producerPeerID string, tuple Can
|
||||
}
|
||||
}
|
||||
|
||||
// candidateValid ports 3903; att is the live attempt when validating it.
|
||||
// candidateValid receives att when validating a live attempt.
|
||||
func (c *Controller) candidateValid(childPeerID string, plan CandidatePlan, att *attempt) bool {
|
||||
tuple := plan.Tuple
|
||||
child, _ := c.participants.Get(childPeerID)
|
||||
@@ -203,7 +202,6 @@ func (c *Controller) candidateValid(childPeerID string, plan CandidatePlan, att
|
||||
return currentPlan != nil && endpointTransitionEquals(currentPlan.EndpointTransition, plan.EndpointTransition)
|
||||
}
|
||||
|
||||
// operationCandidateValid ports 3934.
|
||||
func (c *Controller) operationCandidateValid(op *operation, plan CandidatePlan, att *attempt) bool {
|
||||
return c.candidateValid(op.childPeerID, plan, att) &&
|
||||
((att != nil && att.connectionAttempt != nil) ||
|
||||
@@ -211,7 +209,6 @@ func (c *Controller) operationCandidateValid(op *operation, plan CandidatePlan,
|
||||
c.candidateOpportunityAvailable(op.childPeerID, plan))
|
||||
}
|
||||
|
||||
// physicalCopies ports 3999.
|
||||
func (c *Controller) physicalCopies(peerID string) int {
|
||||
copies := 0
|
||||
for _, edge := range c.upstreamByViewer.Values() {
|
||||
@@ -225,7 +222,7 @@ func (c *Controller) physicalCopies(peerID string) int {
|
||||
return copies
|
||||
}
|
||||
|
||||
// assertReservation ports 4006: panics on a reservation kind that does not
|
||||
// assertReservation panics on a reservation kind that does not
|
||||
// match the tuple or a missing overlap for an overlap transition.
|
||||
func assertReservation(tuple CandidateTuple, reservation CandidateReservation, overlap bool) {
|
||||
expected := ReservationSfuCreate
|
||||
@@ -243,7 +240,7 @@ func assertReservation(tuple CandidateTuple, reservation CandidateReservation, o
|
||||
}
|
||||
}
|
||||
|
||||
// targetFits ports 4380; the parent must exist.
|
||||
// targetFits requires an existing parent.
|
||||
func (c *Controller) targetFits(parentPeerID, childPeerID string) bool {
|
||||
parent, _ := c.participants.Get(parentPeerID)
|
||||
edge, _ := c.upstreamByViewer.Get(childPeerID)
|
||||
@@ -260,7 +257,6 @@ func (c *Controller) targetFits(parentPeerID, childPeerID string) bool {
|
||||
return slots <= parent.effectiveDownstreamCapacity
|
||||
}
|
||||
|
||||
// publicationFits ports 4390.
|
||||
func (c *Controller) publicationFits(childPeerID string, replacing bool) bool {
|
||||
host, _ := c.participants.Get(c.hostPeerID)
|
||||
edge, _ := c.upstreamByViewer.Get(childPeerID)
|
||||
@@ -276,7 +272,6 @@ func (c *Controller) publicationFits(childPeerID string, replacing bool) bool {
|
||||
return host != nil && host.sessionID != "" && !host.departureConfirmed && nextSlots <= host.effectiveDownstreamCapacity
|
||||
}
|
||||
|
||||
// hostHasPublicationSlot ports 4400.
|
||||
func (c *Controller) hostHasPublicationSlot() bool {
|
||||
host, _ := c.participants.Get(c.hostPeerID)
|
||||
return host != nil && c.usedSlots(c.hostPeerID)+1 <= host.effectiveDownstreamCapacity
|
||||
@@ -286,7 +281,7 @@ func (c *Controller) usedSlots(peerID string) int {
|
||||
return c.physicalCopies(peerID)
|
||||
}
|
||||
|
||||
// remaining ports 4409 and may be negative; the parent must exist.
|
||||
// remaining may be negative; the parent must exist.
|
||||
func (c *Controller) remaining(parentPeerID, childPeerID string) int64 {
|
||||
parent, _ := c.participants.Get(parentPeerID)
|
||||
edge, _ := c.upstreamByViewer.Get(childPeerID)
|
||||
@@ -298,8 +293,8 @@ func (c *Controller) remaining(parentPeerID, childPeerID string) int64 {
|
||||
return value
|
||||
}
|
||||
|
||||
// depth ports 4416; a cycle yields MAX_SAFE_INTEGER (not MaxInt64) so the
|
||||
// comparator subtraction cannot overflow.
|
||||
// depth yields MAX_SAFE_INTEGER on a cycle, avoiding MaxInt64 so comparator
|
||||
// subtraction cannot overflow.
|
||||
func (c *Controller) depth(peerID string) int64 {
|
||||
if peerID == c.hostPeerID {
|
||||
return 1
|
||||
@@ -323,8 +318,8 @@ func (c *Controller) depth(peerID string) int64 {
|
||||
}
|
||||
}
|
||||
|
||||
// childrenOf ports 4352: direct peer children in upstreamByViewer order
|
||||
// (§4.2 #17), the base order for every children-derived list.
|
||||
// childrenOf returns direct peer children in upstreamByViewer order,
|
||||
// the base order for every children-derived list.
|
||||
func (c *Controller) childrenOf(parentPeerID string) []string {
|
||||
children := []string{}
|
||||
for childPeerID, edge := range c.upstreamByViewer.All() {
|
||||
@@ -335,8 +330,7 @@ func (c *Controller) childrenOf(parentPeerID string) []string {
|
||||
return children
|
||||
}
|
||||
|
||||
// descendantsOf ports 4356: a DFS (the TS queue is popped from the end,
|
||||
// §4.5 #49) returning the visit order without duplicates.
|
||||
// descendantsOf walks depth-first, returning visit order without duplicates.
|
||||
func (c *Controller) descendantsOf(peerID string) []string {
|
||||
descendants := []string{}
|
||||
seen := make(map[string]struct{})
|
||||
@@ -357,7 +351,7 @@ func (c *Controller) descendantsOf(peerID string) []string {
|
||||
}
|
||||
|
||||
// withSelf ports `this.descendantsOf(peerId).add(peerId)`: the peer itself
|
||||
// is appended last unless the walk already reached it (§4.5 #50).
|
||||
// is appended last unless the walk already reached it.
|
||||
func withSelf(descendants []string, peerID string) []string {
|
||||
if slices.Contains(descendants, peerID) {
|
||||
return descendants
|
||||
@@ -365,7 +359,7 @@ func withSelf(descendants []string, peerID string) []string {
|
||||
return append(descendants, peerID)
|
||||
}
|
||||
|
||||
// sourceUsable ports 4365: the whole upstream path is usable and active.
|
||||
// sourceUsable checks that the whole upstream path is usable and active.
|
||||
func (c *Controller) sourceUsable(peerID string) bool {
|
||||
if peerID == c.hostPeerID {
|
||||
return true
|
||||
@@ -391,12 +385,11 @@ func (c *Controller) sourceUsable(peerID string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// participantBlocked ports 3862.
|
||||
func (c *Controller) participantBlocked(current *participant) bool {
|
||||
return current.availabilityExhausted && len(c.buildCandidates(current.peerID, false)) == 0
|
||||
}
|
||||
|
||||
// availableViewers ports 3869: connected, non-departed viewers sorted by
|
||||
// availableViewers returns connected, non-departed viewers sorted by
|
||||
// compareParticipant (a total order, so the map order does not leak).
|
||||
func (c *Controller) availableViewers(includeBlocked bool) []*participant {
|
||||
viewers := []*participant{}
|
||||
@@ -410,8 +403,8 @@ func (c *Controller) availableViewers(includeBlocked bool) []*participant {
|
||||
return viewers
|
||||
}
|
||||
|
||||
// overflowChildren ports 3876: per parent in participants order, the
|
||||
// overflowing children newest-first (§4.1 #3).
|
||||
// overflowChildren visits parents in participant order and returns each
|
||||
// parent's overflowing children newest first.
|
||||
func (c *Controller) overflowChildren() []string {
|
||||
result := []string{}
|
||||
for _, parent := range c.participants.Values() {
|
||||
@@ -428,8 +421,8 @@ func (c *Controller) overflowChildren() []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// overflowPeerChildren ports 3889: the physically active direct children
|
||||
// beyond the parent's capacity, oldest first kept.
|
||||
// overflowPeerChildren keeps the oldest physically active direct children
|
||||
// within the parent's capacity and returns the overflow.
|
||||
func (c *Controller) overflowPeerChildren(parentPeerID string) []string {
|
||||
parent, _ := c.participants.Get(parentPeerID)
|
||||
if parent == nil {
|
||||
@@ -457,7 +450,6 @@ func (c *Controller) overflowPeerChildren(parentPeerID string) []string {
|
||||
return children[capacity:]
|
||||
}
|
||||
|
||||
// tupleKey ports 4979.
|
||||
func tupleKey(tuple CandidateTuple) string {
|
||||
if tuple.Kind == UpstreamPeer {
|
||||
key := "peer:" + tuple.ParentPeerID
|
||||
@@ -469,7 +461,6 @@ func tupleKey(tuple CandidateTuple) string {
|
||||
return "sfu:" + string(tuple.Publication)
|
||||
}
|
||||
|
||||
// edgeTupleKey ports 4999.
|
||||
func edgeTupleKey(edge *CommittedEdge) string {
|
||||
if edge.Kind == UpstreamPeer {
|
||||
return "peer:" + edge.ParentPeerID
|
||||
@@ -477,7 +468,6 @@ func edgeTupleKey(edge *CommittedEdge) string {
|
||||
return "sfu:reuse"
|
||||
}
|
||||
|
||||
// endpointTransitionRank ports 5003.
|
||||
func endpointTransitionRank(transition EndpointTransition) int {
|
||||
switch transition.Kind {
|
||||
case TransitionNone:
|
||||
@@ -489,7 +479,7 @@ func endpointTransitionRank(transition EndpointTransition) int {
|
||||
}
|
||||
}
|
||||
|
||||
// cloneCandidatePlan ports 5020 (deep copy of the retirement).
|
||||
// cloneCandidatePlan copies the plan and deeply copies its retirement data.
|
||||
func cloneCandidatePlan(plan CandidatePlan) CandidatePlan {
|
||||
clone := plan
|
||||
if plan.EndpointTransition.ProducerPeerID != nil {
|
||||
@@ -503,7 +493,6 @@ func cloneCandidatePlan(plan CandidatePlan) CandidatePlan {
|
||||
return clone
|
||||
}
|
||||
|
||||
// candidatePlanEquals ports 5029.
|
||||
func candidatePlanEquals(left, right CandidatePlan) bool {
|
||||
return tupleKey(left.Tuple) == tupleKey(right.Tuple) &&
|
||||
endpointTransitionEquals(left.EndpointTransition, right.EndpointTransition)
|
||||
@@ -516,7 +505,7 @@ func stringPtrEquals(left, right *string) bool {
|
||||
return *left == *right
|
||||
}
|
||||
|
||||
// endpointTransitionEquals ports 5034; `undefined !== HOST` matters for the
|
||||
// In endpointTransitionEquals, `undefined !== HOST` matters for the
|
||||
// none producer, hence the pointer comparison.
|
||||
func endpointTransitionEquals(left, right EndpointTransition) bool {
|
||||
if left.Kind != right.Kind {
|
||||
@@ -545,7 +534,7 @@ func endpointTransitionEquals(left, right EndpointTransition) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// reservationResources ports 5060 in order: edge (never for a borrowed
|
||||
// reservationResources returns resources in order: edge (never for a borrowed
|
||||
// sfu-reuse edge, which belongs to another holder), publication, overlap.
|
||||
func reservationResources(reservation CandidateReservation) []*Resource {
|
||||
resources := []*Resource{}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
)
|
||||
|
||||
// File-local constants of room-route-controller.ts (lines 112-114).
|
||||
const (
|
||||
maxDirectHeadStartMs int64 = 5_000
|
||||
maxNatConnectionAttempts = 3
|
||||
@@ -118,7 +117,7 @@ func (o *operation) planAt(index int) *CandidatePlan {
|
||||
}
|
||||
|
||||
// planAtOrLast ports `operation.candidates[Math.min(cursor, length - 1)]`,
|
||||
// which is undefined (nil) for an empty array (map R8).
|
||||
// which is undefined (nil) for an empty array.
|
||||
func (o *operation) planAtOrLast() *CandidatePlan {
|
||||
return o.planAt(min(o.cursor, len(o.candidates)-1))
|
||||
}
|
||||
@@ -323,7 +322,7 @@ func (c *Controller) Publication() *HostPublication {
|
||||
// Snapshot returns a copy of the committed graph, the publication and the
|
||||
// operation snapshot; it mutates nothing.
|
||||
func (c *Controller) Snapshot() RouteSnapshot {
|
||||
// Ordering site: the router iterates this map to build assignments (§4.2 #9).
|
||||
// The router iterates this map to build assignments.
|
||||
upstream := &ordered.Map[string, CommittedEdge]{}
|
||||
for id, edge := range c.upstreamByViewer.All() {
|
||||
upstream.Set(id, *edge)
|
||||
@@ -513,7 +512,7 @@ func (c *Controller) RebindHostIdentity(peerID, sessionID string, nowMs *int64)
|
||||
panic(errors.New("Route Host identity is unavailable"))
|
||||
}
|
||||
|
||||
// Ordering site §4.5 #38: aborted reservation, then SFU edges in map
|
||||
// Aborted reservation, then SFU edges in map
|
||||
// order, then the publication, deduplicated.
|
||||
released := &resourceSet{}
|
||||
if c.operation != nil {
|
||||
@@ -541,7 +540,7 @@ func (c *Controller) RebindHostIdentity(peerID, sessionID string, nowMs *int64)
|
||||
c.sfuBootstrapIntent = nil
|
||||
c.consumedSfuBootstrapOpportunities.Clear()
|
||||
c.rootConvergenceRootPeerID = ""
|
||||
// Ordering site §4.1 #2: demand is re-recorded in participants order.
|
||||
// Demand is re-recorded in participants order.
|
||||
for _, current := range c.participants.Values() {
|
||||
current.availabilityExhausted = false
|
||||
current.bootstrapFailureReported = false
|
||||
@@ -552,7 +551,7 @@ func (c *Controller) RebindHostIdentity(peerID, sessionID string, nowMs *int64)
|
||||
}
|
||||
}
|
||||
|
||||
// Ordering site §4.1 #1: delete + set moves the Host to the tail of the
|
||||
// Delete + set moves the Host to the tail of the
|
||||
// insertion order while it keeps its joinOrder.
|
||||
c.participants.Delete(previousPeerID)
|
||||
host.peerID = peerID
|
||||
@@ -672,7 +671,7 @@ func (c *Controller) InvalidateEdge(guard EdgeGuard, nowMs *int64) bool {
|
||||
// InvalidateDirectEdgeFromParent invalidates the first committed direct edge
|
||||
// (insertion order) matching the parent-side guard.
|
||||
func (c *Controller) InvalidateDirectEdgeFromParent(input ParentEdgeGuard, nowMs *int64) bool {
|
||||
// Ordering site §4.2 #10: first match in insertion order wins.
|
||||
// First match in insertion order wins.
|
||||
for childPeerID, edge := range c.upstreamByViewer.All() {
|
||||
if edge.Kind == UpstreamPeer &&
|
||||
edge.ParentPeerID == input.ParentPeerID &&
|
||||
@@ -768,7 +767,7 @@ func (c *Controller) RetireHostPublication(guard PublicationGuard) []*Resource {
|
||||
publication.ConnectionID != guard.ConnectionID {
|
||||
return []*Resource{}
|
||||
}
|
||||
// Ordering site §4.2 #11: subscriptions in map order, then the publication.
|
||||
// Subscriptions in map order, then the publication.
|
||||
released := []*Resource{}
|
||||
for viewerPeerID, edge := range c.upstreamByViewer.All() {
|
||||
if edge.Kind == UpstreamSfu && edge.PublicationGeneration == publication.Generation {
|
||||
@@ -814,7 +813,7 @@ func (c *Controller) SetPaused(paused bool, nowMs *int64) []*Resource {
|
||||
// Dispose clears all state, pauses the controller and returns the live
|
||||
// reservation's resources then every committed resource, deduplicated.
|
||||
func (c *Controller) Dispose() []*Resource {
|
||||
// Ordering site §4.5 #39: reservation first, then committed resources.
|
||||
// Reservation first, then committed resources.
|
||||
resources := &resourceSet{}
|
||||
if c.operation != nil && c.operation.current != nil {
|
||||
resources.add(reservationResources(c.operation.current.reservation)...)
|
||||
@@ -836,14 +835,13 @@ func (c *Controller) Dispose() []*Resource {
|
||||
c.sfuBootstrapIntent = nil
|
||||
c.consumedSfuBootstrapOpportunities.Clear()
|
||||
c.rootConvergenceRootPeerID = ""
|
||||
// nextJoinOrder, revision, latestRevision and factVersion survive (TS 2314-2339).
|
||||
// nextJoinOrder, revision, latestRevision and factVersion survive.
|
||||
c.paused = true
|
||||
return resources.resources()
|
||||
}
|
||||
|
||||
// abortOperation ports the private abortOperation (4050): finishes the
|
||||
// demand timing when nowMs is given, releases the live reservation and
|
||||
// clears the operation.
|
||||
// abortOperation finishes demand timing when nowMs is given, releases the live
|
||||
// reservation and clears the operation.
|
||||
func (c *Controller) abortOperation(nowMs *int64, bucket RejectionBucket) []*Resource {
|
||||
if c.operation == nil {
|
||||
return []*Resource{}
|
||||
@@ -861,7 +859,6 @@ func (c *Controller) abortOperation(nowMs *int64, bucket RejectionBucket) []*Res
|
||||
return released
|
||||
}
|
||||
|
||||
// operationUsesParticipantSession ports 4072.
|
||||
func (c *Controller) operationUsesParticipantSession(peerID string) bool {
|
||||
op := c.operation
|
||||
if op == nil {
|
||||
@@ -877,7 +874,7 @@ func (c *Controller) operationUsesParticipantSession(peerID string) bool {
|
||||
return peerID == c.hostPeerID
|
||||
}
|
||||
|
||||
// effectiveCapacity ports 4925: panics "Effective capacity is invalid" for
|
||||
// effectiveCapacity panics "Effective capacity is invalid" for
|
||||
// a negative or unsafe value and clamps to the endpoint capacity.
|
||||
func (c *Controller) effectiveCapacity(value int) int {
|
||||
if !isSafeInteger(int64(value)) || value < 0 {
|
||||
@@ -888,7 +885,7 @@ func (c *Controller) effectiveCapacity(value int) int {
|
||||
|
||||
func (c *Controller) touchFacts() { c.factVersion++ }
|
||||
|
||||
// allocateRevision ports 4932; the revision space is MAX_SAFE_INTEGER.
|
||||
// allocateRevision bounds revisions by MAX_SAFE_INTEGER.
|
||||
func (c *Controller) allocateRevision() int64 {
|
||||
if c.latestRevision >= protocol.MaxMediaRouteRevision {
|
||||
panic(errors.New("Media route revision space exhausted"))
|
||||
@@ -902,8 +899,8 @@ func isSafeInteger(value int64) bool {
|
||||
return value >= -maxSafeInteger && value <= maxSafeInteger
|
||||
}
|
||||
|
||||
// compareParticipant ports 4939. joinOrder is allocated once per
|
||||
// participant (573-577) and preserved across rebindHostIdentity, so two live
|
||||
// joinOrder is allocated once per participant and preserved across
|
||||
// rebindHostIdentity, so two live
|
||||
// participants never share one and the TS localeCompare (ICU) tiebreak is
|
||||
// unreachable; strings.Compare stands in for it.
|
||||
func compareParticipant(left, right *participant) int {
|
||||
@@ -916,7 +913,7 @@ func compareParticipant(left, right *participant) int {
|
||||
return strings.Compare(left.peerID, right.peerID)
|
||||
}
|
||||
|
||||
// stablePairRank ports 4955: the first 6 bytes, big-endian, of
|
||||
// stablePairRank returns the first 6 bytes, big-endian, of
|
||||
// sha256(child + "\0" + parent). Byte-exact: it breaks parent ties.
|
||||
func stablePairRank(childPeerID, parentPeerID string) int64 {
|
||||
sum := sha256.Sum256([]byte(childPeerID + "\x00" + parentPeerID))
|
||||
@@ -927,8 +924,8 @@ func stablePairRank(childPeerID, parentPeerID string) int64 {
|
||||
return rank
|
||||
}
|
||||
|
||||
// safeAdd ports 4969. JS Math.round is half-up while math.Round is
|
||||
// half-away-from-zero; they differ only for negative halves, which the
|
||||
// JS Math.round is half-up while math.Round is half-away-from-zero;
|
||||
// they differ only for negative halves, which the
|
||||
// `rounded < 0` branch drops either way. NaN and +/-Inf drop the window.
|
||||
func safeAdd(total int64, delta float64) int64 {
|
||||
rounded := math.Round(delta)
|
||||
|
||||
@@ -664,29 +664,77 @@ func TestAbandonsPendingQualityMoveWhenIncumbentRecovers(t *testing.T) {
|
||||
eq(t, edge.ConnectionID, "a_from_host")
|
||||
}
|
||||
|
||||
// TS 2086-2095: when candidateReady abandons a quality move because the
|
||||
// incumbent is no longer degraded, activeRevision is the revision that was
|
||||
// active before the abort (the TS literal reads this.revision before
|
||||
// abortOperation advances it), even though the abort itself advances it.
|
||||
func TestReportsPreAbortRevisionWhenCandidateReadyAbandonsQualityMove(t *testing.T) {
|
||||
routes := newController(2, Options{QualityConvergenceEnabled: true})
|
||||
addViewer(routes, A, 2, nil)
|
||||
addViewer(routes, B, 2, nil)
|
||||
routes.hydrateEdge(A, peerEdge(HOST, "a_from_host"))
|
||||
routes.hydrateEdge(B, peerEdge(HOST, "b_from_host"))
|
||||
observePersistentDegraded(t, routes, A, "a_from_host", 99)
|
||||
routes.Reconcile(103)
|
||||
begin := beginCandidate(t, routes, BeginInput{NowMs: 104, ConnectionID: "a_from_b", Reservation: direct()})
|
||||
current := must(t, begin.Operation).Current
|
||||
before := routes.Snapshot().Revision
|
||||
eq(t, routes.ObserveSenderQualityEvidence(unknownSample(HOST, A, before, "a_from_host", 105), nil).Accepted, true)
|
||||
settled := routes.CandidateReady(guardFor(A, current.Revision, "a_from_b"), 106, nil, CandidateProof{})
|
||||
eq(t, settled.Accepted, true)
|
||||
eq(t, settled.Committed, false)
|
||||
noOperation(t, routes.Snapshot().Operation)
|
||||
eq(t, settled.ActiveRevision, before)
|
||||
if routes.Snapshot().Revision <= before {
|
||||
t.Fatalf("expected the abort to advance the revision past %d, got %d", before, routes.Snapshot().Revision)
|
||||
func TestQualityReadinessKeepsInconclusiveIncumbentWithinTheSameOperation(t *testing.T) {
|
||||
for _, unknownFirst := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("unknown before readiness=%v", unknownFirst), func(t *testing.T) {
|
||||
routes := newController(2, Options{QualityConvergenceEnabled: true})
|
||||
addViewer(routes, A, 2, nil)
|
||||
addViewer(routes, B, 2, nil)
|
||||
routes.hydrateEdge(A, peerEdge(HOST, "a_from_host"))
|
||||
routes.hydrateEdge(B, peerEdge(HOST, "b_from_host"))
|
||||
observePersistentDegraded(t, routes, A, "a_from_host", 99)
|
||||
operation := must(t, routes.Reconcile(103).Operation)
|
||||
begin := beginOperation(t, routes, BeginInput{NowMs: 104, ConnectionID: "a_from_b", Reservation: direct()})
|
||||
guard := guardFor(A, begin.Current.Revision, "a_from_b")
|
||||
unknown := func(at int64) {
|
||||
eq(t, routes.ObserveSenderQualityEvidence(unknownSample(HOST, A, routes.Snapshot().Revision, "a_from_host", at), nil).Accepted, true)
|
||||
}
|
||||
ready := func(at int64) {
|
||||
settled := routes.CandidateReady(guard, at, nil, CandidateProof{RelativeQualityApproved: true})
|
||||
eq(t, settled.Accepted, true)
|
||||
eq(t, settled.Committed, false)
|
||||
}
|
||||
if unknownFirst {
|
||||
unknown(105)
|
||||
ready(106)
|
||||
} else {
|
||||
ready(105)
|
||||
unknown(106)
|
||||
}
|
||||
pending := must(t, routes.Snapshot().Operation)
|
||||
eq(t, pending.Current.Revision, guard.Revision)
|
||||
eq(t, pending.DeadlineAtMs, operation.DeadlineAtMs)
|
||||
eq(t, edgeOf(t, routes, A).ConnectionID, "a_from_host")
|
||||
settled := routes.ObserveSenderQualityEvidence(senderSample(
|
||||
B, A, guard.Revision, guard.ConnectionID, "candidate-rtp\x00track", SenderQualityHealthy, 107), nil)
|
||||
eq(t, settled.Committed, true)
|
||||
noOperation(t, routes.Snapshot().Operation)
|
||||
eq(t, edgeOf(t, routes, A).ConnectionID, "a_from_b")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInconclusiveQualityOperationStillRequiresCurrentAuthority(t *testing.T) {
|
||||
for _, cause := range []string{"healthy", "reset", "sender-replaced", "pause", "deadline"} {
|
||||
t.Run(cause, func(t *testing.T) {
|
||||
routes := newController(2, Options{QualityConvergenceEnabled: true})
|
||||
addViewer(routes, A, 2, nil)
|
||||
addViewer(routes, B, 2, nil)
|
||||
routes.hydrateEdge(A, peerEdge(HOST, "a_from_host"))
|
||||
routes.hydrateEdge(B, peerEdge(HOST, "b_from_host"))
|
||||
observePersistentDegraded(t, routes, A, "a_from_host", 99)
|
||||
operation := must(t, routes.Reconcile(103).Operation)
|
||||
begin := beginOperation(t, routes, BeginInput{NowMs: 104, ConnectionID: "a_from_b", Reservation: direct()})
|
||||
guard := guardFor(A, begin.Current.Revision, "a_from_b")
|
||||
eq(t, routes.ObserveSenderQualityEvidence(unknownSample(HOST, A, routes.Snapshot().Revision, "a_from_host", 105), nil).Accepted, true)
|
||||
eq(t, routes.CandidateReady(guard, 106, nil, CandidateProof{RelativeQualityApproved: true}).Accepted, true)
|
||||
at := int64(107)
|
||||
switch cause {
|
||||
case "healthy":
|
||||
observeSenderState(routes, senderState{childPeerID: A, connectionID: "a_from_host", state: SenderQualityHealthy, acceptedAtMs: at})
|
||||
case "reset":
|
||||
routes.ResetSenderQuality(HOST, "host_session", at)
|
||||
case "sender-replaced":
|
||||
observeSenderState(routes, senderState{childPeerID: A, connectionID: "a_from_host", senderIdentity: "replacement-rtp\x00track", state: SenderQualityDegraded, acceptedAtMs: at})
|
||||
case "pause":
|
||||
routes.SetPaused(true, &at)
|
||||
case "deadline":
|
||||
at = operation.DeadlineAtMs
|
||||
}
|
||||
eq(t, routes.CandidateReady(guard, at, nil, CandidateProof{RelativeQualityApproved: true}).Accepted, false)
|
||||
noOperation(t, routes.Snapshot().Operation)
|
||||
eq(t, edgeOf(t, routes, A).ConnectionID, "a_from_host")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1015,34 +1063,37 @@ func TestRejectsSfuQualityCandidateOnDegradedPublisherEvidence(t *testing.T) {
|
||||
eq(t, edge.ParentPeerID, HOST)
|
||||
}
|
||||
|
||||
// TS 2297: abandons Host relief when another Host root recovers during the canary
|
||||
func TestAbandonsHostReliefWhenAnotherHostRootRecoversDuringCanary(t *testing.T) {
|
||||
routes := newController(2, Options{SfuEnabled: true, QualityConvergenceEnabled: true})
|
||||
addViewer(routes, A, 1, nil)
|
||||
addViewer(routes, B, 0, nil)
|
||||
routes.hydrateEdge(A, peerEdge(HOST, "a_from_host"))
|
||||
routes.hydrateEdge(B, peerEdge(HOST, "b_from_host"))
|
||||
observePersistentDegraded(t, routes, A, "a_from_host", 99)
|
||||
observePersistentDegraded(t, routes, B, "b_from_host", 99)
|
||||
routes.Reconcile(103)
|
||||
begin := beginCandidate(t, routes, BeginInput{
|
||||
NowMs: 104,
|
||||
ConnectionID: "a_from_sfu",
|
||||
Reservation: sfuCreateOverlap("sfu-edge", "sfu-publication", "host-overlap"),
|
||||
PublicationGeneration: "publication_generation_12345678",
|
||||
PublicationConnectionID: "publication_connection_12345678",
|
||||
HostSessionID: "host_session",
|
||||
})
|
||||
current := must(t, begin.Operation).Current
|
||||
eq(t, observeSenderState(routes, senderState{
|
||||
childPeerID: B, connectionID: "b_from_host", state: SenderQualityHealthy, acceptedAtMs: 105,
|
||||
}).Accepted, true)
|
||||
eq(t, routes.CandidateReady(guardFor(A, current.Revision, "a_from_sfu"), 106, nil, CandidateProof{}).Committed, false)
|
||||
noOperation(t, routes.Snapshot().Operation)
|
||||
edge := edgeOf(t, routes, A)
|
||||
eq(t, edge.Kind, UpstreamPeer)
|
||||
eq(t, edge.ParentPeerID, HOST)
|
||||
eq(t, edge.ConnectionID, "a_from_host")
|
||||
func TestAbandonsHostReliefWhenAnotherHostRootIsNoLongerDegraded(t *testing.T) {
|
||||
for _, state := range []SenderQualityState{SenderQualityHealthy, SenderQualityUnknown} {
|
||||
t.Run(string(state), func(t *testing.T) {
|
||||
routes := newController(2, Options{SfuEnabled: true, QualityConvergenceEnabled: true})
|
||||
addViewer(routes, A, 1, nil)
|
||||
addViewer(routes, B, 0, nil)
|
||||
routes.hydrateEdge(A, peerEdge(HOST, "a_from_host"))
|
||||
routes.hydrateEdge(B, peerEdge(HOST, "b_from_host"))
|
||||
observePersistentDegraded(t, routes, A, "a_from_host", 99)
|
||||
observePersistentDegraded(t, routes, B, "b_from_host", 99)
|
||||
routes.Reconcile(103)
|
||||
begin := beginCandidate(t, routes, BeginInput{
|
||||
NowMs: 104,
|
||||
ConnectionID: "a_from_sfu",
|
||||
Reservation: sfuCreateOverlap("sfu-edge", "sfu-publication", "host-overlap"),
|
||||
PublicationGeneration: "publication_generation_12345678",
|
||||
PublicationConnectionID: "publication_connection_12345678",
|
||||
HostSessionID: "host_session",
|
||||
})
|
||||
current := must(t, begin.Operation).Current
|
||||
eq(t, observeSenderState(routes, senderState{
|
||||
childPeerID: B, connectionID: "b_from_host", state: state, acceptedAtMs: 105,
|
||||
}).Accepted, true)
|
||||
eq(t, routes.CandidateReady(guardFor(A, current.Revision, "a_from_sfu"), 106, nil, CandidateProof{}).Committed, false)
|
||||
noOperation(t, routes.Snapshot().Operation)
|
||||
edge := edgeOf(t, routes, A)
|
||||
eq(t, edge.Kind, UpstreamPeer)
|
||||
eq(t, edge.ParentPeerID, HOST)
|
||||
eq(t, edge.ConnectionID, "a_from_host")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TS 2350: uses SFU reuse only for multi-edge Host relief with healthy ingress
|
||||
|
||||
@@ -33,7 +33,7 @@ func (c *Controller) debug(event string, details ...any) {
|
||||
c.debugLog("piik-route", args...)
|
||||
}
|
||||
|
||||
// debugPeer ports 4895: "host", "viewer-<joinOrder>" or "viewer-unknown".
|
||||
// debugPeer returns "host", "viewer-<joinOrder>" or "viewer-unknown".
|
||||
func (c *Controller) debugPeer(peerID string) string {
|
||||
current, _ := c.participants.Get(peerID)
|
||||
if peerID == c.hostPeerID || (current != nil && current.role == protocol.RoleHost) {
|
||||
@@ -45,7 +45,7 @@ func (c *Controller) debugPeer(peerID string) string {
|
||||
return "viewer-" + strconv.FormatInt(current.joinOrder, 10)
|
||||
}
|
||||
|
||||
// debugTuple ports 4903: "p2p:<label>[:regenerate]" or "sfu:<publication>".
|
||||
// debugTuple returns "p2p:<label>[:regenerate]" or "sfu:<publication>".
|
||||
func (c *Controller) debugTuple(tuple CandidateTuple) string {
|
||||
if tuple.Kind == UpstreamPeer {
|
||||
label := "p2p:" + c.debugPeer(tuple.ParentPeerID)
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
)
|
||||
|
||||
// pruneDepartedLeaves ports 4015: removes departed viewers without
|
||||
// children to a fixpoint, in participants order (§4.1 #4), and drops an
|
||||
// pruneDepartedLeaves removes departed viewers without
|
||||
// children to a fixpoint, in participants order, and drops an
|
||||
// orphaned publication.
|
||||
func (c *Controller) pruneDepartedLeaves(released *[]*Resource) []string {
|
||||
removed := []string{}
|
||||
@@ -49,7 +49,6 @@ func (c *Controller) pruneDepartedLeaves(released *[]*Resource) []string {
|
||||
return removed
|
||||
}
|
||||
|
||||
// retireInvalidOperationEdge ports 4133.
|
||||
func (c *Controller) retireInvalidOperationEdge(childPeerID string, released *[]*Resource) bool {
|
||||
edge, _ := c.upstreamByViewer.Get(childPeerID)
|
||||
if edge == nil || !c.edgeRequiresMove(childPeerID, edge) {
|
||||
@@ -74,7 +73,6 @@ func (c *Controller) retireInvalidOperationEdge(childPeerID string, released *[]
|
||||
return true
|
||||
}
|
||||
|
||||
// edgeRequiresMove ports 4153.
|
||||
func (c *Controller) edgeRequiresMove(childPeerID string, edge *CommittedEdge) bool {
|
||||
if !edge.Usable || !edge.PhysicalActive {
|
||||
return true
|
||||
@@ -95,11 +93,8 @@ func (c *Controller) edgeRequiresMove(childPeerID string, edge *CommittedEdge) b
|
||||
return false
|
||||
}
|
||||
|
||||
// rebindCommittedSession ports 4747: the reconnecting peer keeps its edges
|
||||
// and publication, which take its new session id. The TS `{released,
|
||||
// retired}` result is dropped because it was dead: nothing ever joined the
|
||||
// released set and retired was the constant false, so the caller's
|
||||
// `rebound.retired &&` branch was unreachable (map R12).
|
||||
// rebindCommittedSession preserves the reconnecting peer's edges and
|
||||
// publication, updating them to its new session ID.
|
||||
func (c *Controller) rebindCommittedSession(peerID, sessionID string) {
|
||||
if ownEdge, ok := c.upstreamByViewer.Get(peerID); ok {
|
||||
ownEdge.ChildSessionID = sessionID
|
||||
@@ -115,7 +110,6 @@ func (c *Controller) rebindCommittedSession(peerID, sessionID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// removePublicationGeneration ports 4766 (§4.2 #12).
|
||||
func (c *Controller) removePublicationGeneration(generation string) []*Resource {
|
||||
released := []*Resource{}
|
||||
for viewerPeerID, edge := range c.upstreamByViewer.All() {
|
||||
@@ -140,7 +134,7 @@ func (c *Controller) retainsAnchor(viewerPeerID string) bool {
|
||||
return len(c.childrenOf(viewerPeerID)) > 0
|
||||
}
|
||||
|
||||
// retireSfuEdge ports 4780: releases the subscription only while it is
|
||||
// retireSfuEdge releases the subscription only while it is
|
||||
// still physically active, then keeps the edge as an anchor for attached
|
||||
// peer descendants or deletes it.
|
||||
func (c *Controller) retireSfuEdge(viewerPeerID string, edge *CommittedEdge, released *[]*Resource, retainAnchor bool) {
|
||||
@@ -168,7 +162,6 @@ func (c *Controller) hasRetiringAnchor(generation string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// pruneRetiringSfuAnchors ports 4797 (§4.2 #18, §4.5 #46).
|
||||
func (c *Controller) pruneRetiringSfuAnchors() {
|
||||
removed := true
|
||||
for removed {
|
||||
@@ -190,8 +183,8 @@ func (c *Controller) pruneRetiringSfuAnchors() {
|
||||
}
|
||||
}
|
||||
|
||||
// assertGraph ports 4827: programming-error panics with the TS messages
|
||||
// (§4.2 #20, §4.1 #6 decide which violation is reported first).
|
||||
// assertGraph panics on programming errors. Edge insertion order and
|
||||
// participant order determine which violation is reported first.
|
||||
func (c *Controller) assertGraph() {
|
||||
host, _ := c.participants.Get(c.hostPeerID)
|
||||
if host == nil || host.role != protocol.RoleHost {
|
||||
@@ -248,19 +241,16 @@ func (c *Controller) assertGraph() {
|
||||
}
|
||||
}
|
||||
|
||||
// assertViewer ports 4891.
|
||||
func (c *Controller) assertViewer(peerID string) {
|
||||
if current, ok := c.participants.Get(peerID); !ok || current.role != protocol.RoleViewer {
|
||||
panic(errors.New("Route child must be a Viewer"))
|
||||
}
|
||||
}
|
||||
|
||||
// hasSfuSubscribers ports 4431.
|
||||
func (c *Controller) hasSfuSubscribers() bool {
|
||||
return c.sfuSubscriberCount() > 0
|
||||
}
|
||||
|
||||
// sfuSubscriberCount ports 4435.
|
||||
func (c *Controller) sfuSubscriberCount() int {
|
||||
count := 0
|
||||
for _, edge := range c.upstreamByViewer.Values() {
|
||||
@@ -271,8 +261,8 @@ func (c *Controller) sfuSubscriberCount() int {
|
||||
return count
|
||||
}
|
||||
|
||||
// committedResources ports 4439: SFU subscriptions in map order, then the
|
||||
// publication (§4.2 #13).
|
||||
// committedResources returns SFU subscriptions in map order, then the
|
||||
// publication.
|
||||
func (c *Controller) committedResources() []*Resource {
|
||||
resources := []*Resource{}
|
||||
for _, edge := range c.upstreamByViewer.Values() {
|
||||
|
||||
@@ -6,8 +6,7 @@ import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// validation is the private validateOrAdvance result. The TS `exhausted`
|
||||
// flag was written (2454, 2588) but never read, so it is not ported.
|
||||
// validation is the private validateOrAdvance result.
|
||||
type validation struct {
|
||||
released []*Resource
|
||||
exhaustedChildPeerID string // "" = undefined
|
||||
@@ -15,7 +14,7 @@ type validation struct {
|
||||
consumedGuard bool
|
||||
}
|
||||
|
||||
// failedPeerIDsFrom ports 4943: dedupe preserving argument order.
|
||||
// failedPeerIDsFrom deduplicates while preserving argument order.
|
||||
func failedPeerIDsFrom(results ...validation) []string {
|
||||
ids := make([]string, 0, len(results))
|
||||
for _, result := range results {
|
||||
@@ -36,7 +35,7 @@ func isAvailabilityOperation(reason DemandReason) bool {
|
||||
return reason != DemandSfuBootstrap && !isBackgroundConvergence(reason)
|
||||
}
|
||||
|
||||
// operationRequiresNativeCandidateProof ports 3554 (nil-safe).
|
||||
// operationRequiresNativeCandidateProof returns false for a nil operation.
|
||||
func operationRequiresNativeCandidateProof(op *operation) bool {
|
||||
if op == nil {
|
||||
return false
|
||||
@@ -45,7 +44,7 @@ func operationRequiresNativeCandidateProof(op *operation) bool {
|
||||
(op.reason == DemandQualityConvergence && op.current != nil && op.current.tuple.Kind == UpstreamSfu)
|
||||
}
|
||||
|
||||
// directHeadStartMs ports 3535: min(5000, max(1, floor(timeout / 2))).
|
||||
// directHeadStartMs bounds half the operation timeout to 1..5000 ms.
|
||||
func (c *Controller) directHeadStartMs() int64 {
|
||||
return min(maxDirectHeadStartMs, max(1, c.operationTimeoutMs/2))
|
||||
}
|
||||
@@ -75,7 +74,7 @@ func (c *Controller) Reconcile(nowMs int64) ReconcileResult {
|
||||
}
|
||||
removedPeerIDs := c.pruneDepartedLeaves(&released)
|
||||
|
||||
// The loop bound is a count, not an order (§4.1 #8).
|
||||
// The loop bound is a count, not an order.
|
||||
for remaining := c.participants.Len(); remaining > 0; remaining-- {
|
||||
bootstrap := c.takeSfuBootstrapCarrier(nowMs, &failedPeerIDs)
|
||||
routeChildPeerID := ""
|
||||
@@ -141,7 +140,7 @@ func (c *Controller) Reconcile(nowMs int64) ReconcileResult {
|
||||
candidates = c.buildCandidates(childPeerID, bootstrap != nil)
|
||||
}
|
||||
if bootstrap == nil && continuation == nil && demand.sfuFirstAtNextRoute {
|
||||
// Ordering site §4.4 #30: the SFU candidate moves to the front.
|
||||
// The SFU candidate moves to the front.
|
||||
sfuIndex := slices.IndexFunc(candidates, func(candidate CandidatePlan) bool {
|
||||
return candidate.Tuple.Kind == UpstreamSfu
|
||||
})
|
||||
@@ -154,7 +153,7 @@ func (c *Controller) Reconcile(nowMs int64) ReconcileResult {
|
||||
}
|
||||
}
|
||||
if reason == DemandSfuBootstrap {
|
||||
// Ordering site §4.4 #31: bootstrap sets drop bounded-gap candidates.
|
||||
// Bootstrap sets drop bounded-gap candidates.
|
||||
filtered := make([]CandidatePlan, 0, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
if candidate.EndpointTransition.Kind != TransitionBoundedGap {
|
||||
@@ -509,22 +508,8 @@ func (c *Controller) CandidateReady(guard CandidateGuard, nowMs int64, commitRes
|
||||
}
|
||||
nativeCandidateProof := operationRequiresNativeCandidateProof(op)
|
||||
if relativeP2pApproved || relativeP2pPending || nativeCandidateProof {
|
||||
// validateOrAdvance already owns the ongoing operation's eligibility.
|
||||
att.mediaReady = true
|
||||
currentEdge, _ := c.upstreamByViewer.Get(op.childPeerID)
|
||||
if (op.reason == DemandQualityConvergence &&
|
||||
(currentEdge == nil || c.senderQualityState(op.childPeerID, currentEdge, nowMs) != SenderQualityDegraded)) ||
|
||||
(op.reason == DemandRootConvergence && !c.rootConvergenceOperationStillEligible(op)) {
|
||||
// TS 2086-2095: the object literal reads `activeRevision: this.revision`
|
||||
// before `released: [...this.abortOperation(...)]` advances it.
|
||||
activeRevision := c.revision
|
||||
return SettleResult{
|
||||
Accepted: true,
|
||||
Committed: false,
|
||||
FailedPeerIDs: failedPeerIDsFrom(validation),
|
||||
ActiveRevision: activeRevision,
|
||||
Released: concatResources(validation.released, c.abortOperation(&nowMs, RejectionAborted)),
|
||||
}
|
||||
}
|
||||
}
|
||||
if relativeP2pExpired {
|
||||
// A one-shot client proof cannot be renewed by a late sender sample.
|
||||
@@ -612,7 +597,7 @@ func (c *Controller) CandidateReady(guard CandidateGuard, nowMs int64, commitRes
|
||||
"child", c.debugPeer(op.childPeerID),
|
||||
"candidate", c.debugTuple(att.tuple),
|
||||
"revision", att.revision)
|
||||
// Ordering site §4.2 #16: displaced SFU children in map order.
|
||||
// Displaced SFU children in map order.
|
||||
displacedSfuChildren := []string{}
|
||||
if att.tuple.Kind == UpstreamSfu && att.tuple.Publication == PublicationReplace {
|
||||
for childPeerID, edge := range c.upstreamByViewer.All() {
|
||||
@@ -720,7 +705,7 @@ func (c *Controller) OperationExpired(nowMs int64) SettleResult {
|
||||
}
|
||||
}
|
||||
|
||||
// validateOrAdvance ports 2342: the single place that expires, replans and
|
||||
// validateOrAdvance is the single place that expires, replans and
|
||||
// clears the operation. failedGuard names an attempt the caller failed.
|
||||
func (c *Controller) validateOrAdvance(nowMs int64, failedGuard *CandidateGuard) validation {
|
||||
activeRevisionAtStart := c.revision
|
||||
@@ -910,7 +895,7 @@ func (c *Controller) validateOrAdvance(nowMs int64, failedGuard *CandidateGuard)
|
||||
}
|
||||
|
||||
// expiredFinalRoute is the finalRoute expression shared by both
|
||||
// validateOrAdvance terminal branches (2434-2438, 2573-2577).
|
||||
// validateOrAdvance terminal branches.
|
||||
func (c *Controller) expiredFinalRoute(op *operation, backgroundConvergence, exhausted bool) FinalRoute {
|
||||
if backgroundConvergence {
|
||||
return c.currentFinalRoute(op.demandPeerID)
|
||||
@@ -921,9 +906,9 @@ func (c *Controller) expiredFinalRoute(op *operation, backgroundConvergence, exh
|
||||
return FinalRouteWaiting
|
||||
}
|
||||
|
||||
// commitAttempt ports 2600: installs the attempt as the child's committed
|
||||
// commitAttempt installs the attempt as the child's committed
|
||||
// edge and returns the resources committed before but not after, plus the
|
||||
// overlap (§4.2 #14).
|
||||
// overlap.
|
||||
func (c *Controller) commitAttempt(op *operation, att *attempt) []*Resource {
|
||||
before := &resourceSet{}
|
||||
before.add(c.committedResources()...)
|
||||
@@ -940,7 +925,7 @@ func (c *Controller) commitAttempt(op *operation, att *attempt) []*Resource {
|
||||
c.clearQualityForParticipant(op.childPeerID)
|
||||
|
||||
if att.tuple.Kind == UpstreamSfu && att.tuple.Publication == PublicationReplace {
|
||||
// The return value is discarded on purpose (2616): the before/after
|
||||
// The return value is discarded on purpose: the before/after
|
||||
// diff below picks those resources up.
|
||||
c.removePublicationGeneration(c.hostPublication.Generation)
|
||||
}
|
||||
@@ -950,7 +935,7 @@ func (c *Controller) commitAttempt(op *operation, att *attempt) []*Resource {
|
||||
if parent == nil || parent.sessionID == "" {
|
||||
panic(errors.New("Candidate parent session is unavailable"))
|
||||
}
|
||||
// Map.set keeps an existing child's position (§4.2 #15).
|
||||
// Map.set keeps an existing child's position.
|
||||
c.upstreamByViewer.Set(op.childPeerID, &CommittedEdge{
|
||||
Kind: UpstreamPeer,
|
||||
ChildSessionID: op.childSessionID,
|
||||
@@ -1013,7 +998,7 @@ func (c *Controller) commitAttempt(op *operation, att *attempt) []*Resource {
|
||||
remainingParentPeerIDs = append(remainingParentPeerIDs, op.deferredParentPeerIDs...)
|
||||
}
|
||||
if len(remainingParentPeerIDs) > 0 {
|
||||
// Ordering site §4.3 #27: dedupe preserving first occurrence.
|
||||
// Dedupe preserving first occurrence.
|
||||
c.directContinuations.Set(op.childPeerID, &directContinuation{
|
||||
childSessionID: op.childSessionID,
|
||||
sfuConnectionID: att.connectionID,
|
||||
@@ -1100,7 +1085,6 @@ func (c *Controller) commitAttempt(op *operation, att *attempt) []*Resource {
|
||||
return released
|
||||
}
|
||||
|
||||
// advanceActiveRevision ports 4088.
|
||||
func (c *Controller) advanceActiveRevision(op *operation) {
|
||||
if op.reason == DemandQualityConvergence && op.current != nil {
|
||||
producerPeerID := c.hostPeerID
|
||||
@@ -1123,7 +1107,6 @@ func (c *Controller) advanceActiveRevision(op *operation) {
|
||||
op.baseRevision = c.revision
|
||||
}
|
||||
|
||||
// blockAndClear ports 4112.
|
||||
func (c *Controller) blockAndClear(op *operation, block bool, released *[]*Resource, revisionAdvanced bool) {
|
||||
child, _ := c.participants.Get(op.childPeerID)
|
||||
c.operation = nil
|
||||
@@ -1143,10 +1126,9 @@ func (c *Controller) blockAndClear(op *operation, block bool, released *[]*Resou
|
||||
child.availabilityExhausted = true
|
||||
}
|
||||
|
||||
// replanRemaining ports 4164: re-plans the suffix from the cursor, forcing
|
||||
// replanRemaining replans the suffix from the cursor, forcing
|
||||
// firstTuple into the first slot and appending restoreTuple when absent.
|
||||
func (c *Controller) replanRemaining(op *operation, firstTuple, restoreTuple *CandidateTuple) {
|
||||
// Ordering site §4.4 #34.
|
||||
cursor := min(op.cursor, len(op.candidates))
|
||||
prefix := append([]CandidatePlan{}, op.candidates[:cursor]...)
|
||||
tuples := make([]CandidateTuple, 0, len(op.candidates)-cursor+2)
|
||||
@@ -1177,7 +1159,7 @@ func (c *Controller) replanRemaining(op *operation, firstTuple, restoreTuple *Ca
|
||||
op.builtAtFactVersion = c.factVersion
|
||||
}
|
||||
|
||||
// operationSnapshot ports 4190: a deep copy plus the derived wakeAtMs;
|
||||
// operationSnapshot returns a deep copy plus the derived wakeAtMs;
|
||||
// Current omits ConnectionAttempt when undefined.
|
||||
func (c *Controller) operationSnapshot() *OperationSnapshot {
|
||||
op := c.operation
|
||||
@@ -1215,7 +1197,6 @@ func (c *Controller) operationSnapshot() *OperationSnapshot {
|
||||
return snapshot
|
||||
}
|
||||
|
||||
// advanceExpiredDirectHeadStart ports 4208.
|
||||
func (c *Controller) advanceExpiredDirectHeadStart(op *operation, nowMs int64, released *[]*Resource) bool {
|
||||
currentTuple := op.currentTuple()
|
||||
sfuIndex := c.foregroundSfuIndex(op)
|
||||
@@ -1238,7 +1219,7 @@ func (c *Controller) advanceExpiredDirectHeadStart(op *operation, nowMs int64, r
|
||||
op.current = nil
|
||||
c.clearCandidateTiming(op.demandPeerID)
|
||||
}
|
||||
// Ordering site §4.4 #33: deferred parents keep candidate order.
|
||||
// Deferred parents keep candidate order.
|
||||
for _, candidate := range op.candidates[op.cursor:min(sfuIndex, len(op.candidates))] {
|
||||
if candidate.Tuple.Kind == UpstreamPeer {
|
||||
op.deferredParentPeerIDs = append(op.deferredParentPeerIDs, candidate.Tuple.ParentPeerID)
|
||||
@@ -1254,7 +1235,6 @@ func (c *Controller) advanceExpiredDirectHeadStart(op *operation, nowMs int64, r
|
||||
return true
|
||||
}
|
||||
|
||||
// operationWakeAt ports 4258.
|
||||
func (c *Controller) operationWakeAt(op *operation) int64 {
|
||||
currentTuple := op.currentTuple()
|
||||
if currentTuple != nil && currentTuple.Kind == UpstreamPeer &&
|
||||
@@ -1270,17 +1250,15 @@ func (c *Controller) operationWakeAt(op *operation) int64 {
|
||||
return op.deadlineAtMs
|
||||
}
|
||||
|
||||
// peerCandidateHasSuccessor ports 4281.
|
||||
func (c *Controller) peerCandidateHasSuccessor(op *operation) bool {
|
||||
return op.cursor+1 < len(op.candidates) || c.foregroundSfuIndex(op) > op.cursor
|
||||
}
|
||||
|
||||
// directHeadStartDeadlineAt ports 4290.
|
||||
func (c *Controller) directHeadStartDeadlineAt(op *operation) int64 {
|
||||
return op.deadlineAtMs - c.operationTimeoutMs + c.directHeadStartMs()
|
||||
}
|
||||
|
||||
// foregroundSfuIndex ports 4299: the index of the first SFU candidate at or
|
||||
// foregroundSfuIndex returns the index of the first SFU candidate at or
|
||||
// after the cursor, the candidate count when a bootstrap carrier exists, -1
|
||||
// otherwise (and always -1 for background convergence).
|
||||
func (c *Controller) foregroundSfuIndex(op *operation) int {
|
||||
@@ -1298,7 +1276,6 @@ func (c *Controller) foregroundSfuIndex(op *operation) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
// promoteNextDirectWithinHeadStart ports 4314 (§4.4 #32).
|
||||
func (c *Controller) promoteNextDirectWithinHeadStart(op *operation, nowMs int64) {
|
||||
sfuIndex := c.foregroundSfuIndex(op)
|
||||
if sfuIndex != op.cursor+1 || nowMs >= c.directHeadStartDeadlineAt(op) {
|
||||
@@ -1319,13 +1296,11 @@ func (c *Controller) promoteNextDirectWithinHeadStart(op *operation, nowMs int64
|
||||
op.candidates = slices.Insert(op.candidates, sfuIndex, nextDirect)
|
||||
}
|
||||
|
||||
// guardMatches ports 4338.
|
||||
func (c *Controller) guardMatches(guard CandidateGuard, op *operation, att *attempt) bool {
|
||||
return guard.ChildPeerID == op.childPeerID && guard.ChildSessionID == op.childSessionID &&
|
||||
guard.Revision == att.revision && guard.ConnectionID == att.connectionID
|
||||
}
|
||||
|
||||
// cursorGuardMatches ports 4343.
|
||||
func (c *Controller) cursorGuardMatches(guard CandidateCursorGuard, op *operation) bool {
|
||||
plan := op.planAt(op.cursor)
|
||||
return plan != nil &&
|
||||
@@ -1335,8 +1310,8 @@ func (c *Controller) cursorGuardMatches(guard CandidateCursorGuard, op *operatio
|
||||
candidatePlanEquals(guard.Plan, *plan)
|
||||
}
|
||||
|
||||
// selectNextChild ports 3451: five priority buckets, first-occurrence
|
||||
// dedupe, then a STABLE sort by started attempts (§4.5 #42).
|
||||
// selectNextChild uses five priority buckets, first-occurrence deduplication,
|
||||
// then a stable sort by started attempts.
|
||||
func (c *Controller) selectNextChild() string {
|
||||
viewers := c.availableViewers(false)
|
||||
departedParents := []string{}
|
||||
@@ -1378,7 +1353,7 @@ type directContinuationPick struct {
|
||||
plan CandidatePlan
|
||||
}
|
||||
|
||||
// selectDirectContinuation ports 3477 (§4.3 #23-25): a stable sort over
|
||||
// selectDirectContinuation uses a stable sort over
|
||||
// the round-robin map order, then the first valid parent per child.
|
||||
func (c *Controller) selectDirectContinuation() *directContinuationPick {
|
||||
type entry struct {
|
||||
@@ -1432,9 +1407,9 @@ func (c *Controller) selectDirectContinuation() *directContinuationPick {
|
||||
return nil
|
||||
}
|
||||
|
||||
// consumeDirectContinuationCandidate ports 3564: removes the tried parent
|
||||
// consumeDirectContinuationCandidate removes the tried parent
|
||||
// from the retry order (re-appending it while NAT attempts remain) and
|
||||
// moves the continuation to the tail of the round-robin (§4.3 #24, #26).
|
||||
// moves the continuation to the tail of the round-robin.
|
||||
func (c *Controller) consumeDirectContinuationCandidate(op *operation) {
|
||||
tuple := op.currentTuple()
|
||||
if op.reason != DemandDirectConvergence || tuple == nil || tuple.Kind != UpstreamPeer {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
)
|
||||
|
||||
// candidateOpportunityBase ports 2900: the NUL-joined ledger key. A session
|
||||
// candidateOpportunityBase returns the NUL-joined ledger key. A session
|
||||
// change erases the key because the session is part of it.
|
||||
func (c *Controller) candidateOpportunityBase(childPeerID string, plan CandidatePlan) string {
|
||||
childSessionID := ""
|
||||
@@ -28,7 +28,6 @@ func (c *Controller) candidateOpportunityBase(childPeerID string, plan Candidate
|
||||
return sfuOpportunityPrefix + childSessionID + "\x00" + hostSessionID
|
||||
}
|
||||
|
||||
// candidateOpportunityAvailable ports 2823.
|
||||
func (c *Controller) candidateOpportunityAvailable(childPeerID string, plan CandidatePlan) bool {
|
||||
child, _ := c.participants.Get(childPeerID)
|
||||
if child == nil {
|
||||
@@ -43,7 +42,7 @@ func (c *Controller) candidateOpportunityAvailable(childPeerID string, plan Cand
|
||||
consumed.startedAttempts < maxNatConnectionAttempts)
|
||||
}
|
||||
|
||||
// startCandidateOpportunity ports 2842: counts one NAT attempt for a peer
|
||||
// startCandidateOpportunity counts one NAT attempt for a peer
|
||||
// candidate of an availability or direct-convergence operation.
|
||||
func (c *Controller) startCandidateOpportunity(op *operation, plan CandidatePlan) *ConnectionAttemptProgress {
|
||||
if !c.natPredictionEnabled ||
|
||||
@@ -64,13 +63,11 @@ func (c *Controller) startCandidateOpportunity(op *operation, plan CandidatePlan
|
||||
if ok {
|
||||
newRank = min(previous.rank, rank)
|
||||
}
|
||||
// The new entry has no `exhausted` (2859-2862).
|
||||
opportunities.Set(key, candidateOpportunity{rank: newRank, startedAttempts: startedAttempts})
|
||||
op.peerAttemptStarted = true
|
||||
return &ConnectionAttemptProgress{Current: startedAttempts, Total: maxNatConnectionAttempts}
|
||||
}
|
||||
|
||||
// peerAttemptsStarted ports 2867.
|
||||
func (c *Controller) peerAttemptsStarted(childPeerID, parentPeerID string) int {
|
||||
if !c.natPredictionEnabled {
|
||||
return 0
|
||||
@@ -90,7 +87,7 @@ func (c *Controller) peerAttemptsStarted(childPeerID, parentPeerID string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// viewerAttemptsStarted ports 2880: the sum over every peer key.
|
||||
// viewerAttemptsStarted sums the attempts over every peer key.
|
||||
func (c *Controller) viewerAttemptsStarted(peerID string) int {
|
||||
if !c.natPredictionEnabled {
|
||||
return 0
|
||||
@@ -108,7 +105,6 @@ func (c *Controller) viewerAttemptsStarted(peerID string) int {
|
||||
return started
|
||||
}
|
||||
|
||||
// hasRemainingNatOpportunity ports 2890.
|
||||
func (c *Controller) hasRemainingNatOpportunity(op *operation) bool {
|
||||
if !isAvailabilityOperation(op.reason) || !op.peerAttemptStarted {
|
||||
return false
|
||||
@@ -121,7 +117,7 @@ func (c *Controller) hasRemainingNatOpportunity(op *operation) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// consumeCandidateOpportunity ports 2916: exhausts the key at its best rank.
|
||||
// consumeCandidateOpportunity exhausts the key at its best rank.
|
||||
func (c *Controller) consumeCandidateOpportunity(childPeerID string, plan CandidatePlan) {
|
||||
child, _ := c.participants.Get(childPeerID)
|
||||
if child == nil {
|
||||
@@ -138,7 +134,6 @@ func (c *Controller) consumeCandidateOpportunity(childPeerID string, plan Candid
|
||||
child.consumedCandidateOpportunities.Set(key, entry)
|
||||
}
|
||||
|
||||
// qualityCandidateOpportunityAvailable ports 2934.
|
||||
func (c *Controller) qualityCandidateOpportunityAvailable(childPeerID string, plan CandidatePlan) bool {
|
||||
observation := c.senderQualityObservations[childPeerID]
|
||||
if observation == nil {
|
||||
@@ -148,7 +143,6 @@ func (c *Controller) qualityCandidateOpportunityAvailable(childPeerID string, pl
|
||||
return !ok || endpointTransitionRank(plan.EndpointTransition) < consumed
|
||||
}
|
||||
|
||||
// consumeQualityCandidateOpportunity ports 2949.
|
||||
func (c *Controller) consumeQualityCandidateOpportunity(op *operation, plan *CandidatePlan) {
|
||||
if op.reason != DemandQualityConvergence || plan == nil {
|
||||
return
|
||||
@@ -162,7 +156,6 @@ func (c *Controller) consumeQualityCandidateOpportunity(op *operation, plan *Can
|
||||
endpointTransitionRank(plan.EndpointTransition))
|
||||
}
|
||||
|
||||
// consumeQualityOperationCandidates ports 2967.
|
||||
func (c *Controller) consumeQualityOperationCandidates(op *operation) {
|
||||
for index := range op.candidates {
|
||||
candidate := op.candidates[index]
|
||||
@@ -170,8 +163,7 @@ func (c *Controller) consumeQualityOperationCandidates(op *operation) {
|
||||
}
|
||||
}
|
||||
|
||||
// consumeActiveEdgeOpportunity ports 2975: an invalidated active edge
|
||||
// exhausts its exact tuple at rank 0.
|
||||
// consumeActiveEdgeOpportunity exhausts the invalidated edge's exact tuple at rank 0.
|
||||
func (c *Controller) consumeActiveEdgeOpportunity(child *participant, edge *CommittedEdge) {
|
||||
if edge.Kind == UpstreamPeer {
|
||||
key := "peer:" + edge.ParentPeerID + "\x00" + child.sessionID + "\x00" + edge.ParentSessionID
|
||||
@@ -189,7 +181,6 @@ func (c *Controller) consumeActiveEdgeOpportunity(child *participant, edge *Comm
|
||||
})
|
||||
}
|
||||
|
||||
// consumeAvailabilityOperation ports 2993.
|
||||
func (c *Controller) consumeAvailabilityOperation(op *operation) {
|
||||
if !isAvailabilityOperation(op.reason) {
|
||||
return
|
||||
@@ -202,7 +193,6 @@ func (c *Controller) consumeAvailabilityOperation(op *operation) {
|
||||
}
|
||||
}
|
||||
|
||||
// consumeCurrentAvailabilityOpportunity ports 3003.
|
||||
func (c *Controller) consumeCurrentAvailabilityOpportunity(op *operation) {
|
||||
if !isAvailabilityOperation(op.reason) {
|
||||
return
|
||||
@@ -216,8 +206,8 @@ func (c *Controller) consumeCurrentAvailabilityOpportunity(op *operation) {
|
||||
}
|
||||
}
|
||||
|
||||
// clearOpportunitiesForSessionChange ports 4561 (§4.1 #5, §4.5 #47): keys
|
||||
// are snapshotted before deletion, which JS defines as safe in place.
|
||||
// clearOpportunitiesForSessionChange snapshots keys before deletion,
|
||||
// preserving JavaScript's safe deletion during iteration.
|
||||
func (c *Controller) clearOpportunitiesForSessionChange(peerID string, role protocol.Role) {
|
||||
if current, ok := c.participants.Get(peerID); ok {
|
||||
current.consumedCandidateOpportunities.Clear()
|
||||
@@ -248,7 +238,6 @@ func (c *Controller) clearOpportunitiesForSessionChange(peerID string, role prot
|
||||
}
|
||||
}
|
||||
|
||||
// clearSfuCandidateOpportunities ports 4597.
|
||||
func (c *Controller) clearSfuCandidateOpportunities() {
|
||||
for _, current := range c.participants.Values() {
|
||||
for _, key := range current.consumedCandidateOpportunities.Keys() {
|
||||
@@ -261,7 +250,7 @@ func (c *Controller) clearSfuCandidateOpportunities() {
|
||||
c.consumedSfuBootstrapOpportunities.Clear()
|
||||
}
|
||||
|
||||
// consumeOpportunity ports 5011: keeps the best (lowest) rank.
|
||||
// consumeOpportunity keeps the best (lowest) rank.
|
||||
func consumeOpportunity(consumed *ordered.Map[string, int], base string, rank int) {
|
||||
previous, ok := consumed.Get(base)
|
||||
if !ok || rank < previous {
|
||||
|
||||
@@ -259,7 +259,6 @@ func (c *Controller) ObserveSenderQualityEvidence(input SenderQualityEvidenceInp
|
||||
return c.senderAccepted([]*Resource{})
|
||||
}
|
||||
|
||||
// senderQualityRegenerationBlockMatches ports 4985.
|
||||
func senderQualityRegenerationBlockMatches(block senderQualityIdentity, childSessionID string, input SenderQualityEvidenceInput, senderIdentity string) bool {
|
||||
return block.childSessionID == childSessionID &&
|
||||
block.parentPeerID == input.ParentPeerID &&
|
||||
@@ -271,7 +270,7 @@ func senderQualityRegenerationBlockMatches(block senderQualityIdentity, childSes
|
||||
// ObserveSfuPublisherQualityEvidence records a Host->SFU ingress sample,
|
||||
// settling a live sfu/reuse quality candidate; a nil commitReservation
|
||||
// always commits. Requires QualityConvergenceEnabled. Unlike the other two
|
||||
// observers it validates no numbers (TS asymmetry, map R13).
|
||||
// observers it validates no numbers.
|
||||
func (c *Controller) ObserveSfuPublisherQualityEvidence(input SfuPublisherQualityEvidenceInput, commitReservation func(CandidateReservation) bool) SenderQualityEvidenceResult {
|
||||
if commitReservation == nil {
|
||||
commitReservation = func(CandidateReservation) bool { return true }
|
||||
@@ -386,7 +385,7 @@ func (c *Controller) ObserveSfuPublisherQualityEvidence(input SfuPublisherQualit
|
||||
return c.senderAccepted([]*Resource{})
|
||||
}
|
||||
|
||||
// settleCandidateQuality ports 1428: folds a sender sample into the live
|
||||
// settleCandidateQuality folds a sender sample into the live
|
||||
// candidate and fails or commits it. senderIdentity "" is the TS null.
|
||||
func (c *Controller) settleCandidateQuality(op *operation, att *attempt, state SenderQualityState, acceptedAtMs int64, senderIdentity string, sampleTimestampMs *int64, commitReservation func(CandidateReservation) bool) SenderQualityEvidenceResult {
|
||||
if att.senderQualityState != state {
|
||||
@@ -486,7 +485,7 @@ func (c *Controller) ResetSenderQuality(parentPeerID, parentSessionID string, no
|
||||
if parent == nil || !parent.sessionIs(parentSessionID) {
|
||||
return []*Resource{}
|
||||
}
|
||||
// Membership only (§4.5 #48): every affected child is deleted.
|
||||
// Membership only: every affected child is deleted.
|
||||
children := make(map[string]struct{})
|
||||
for _, childPeerID := range c.childrenOf(parentPeerID) {
|
||||
children[childPeerID] = struct{}{}
|
||||
@@ -531,7 +530,6 @@ func (c *Controller) ResetSenderQuality(parentPeerID, parentSessionID string, no
|
||||
return []*Resource{}
|
||||
}
|
||||
|
||||
// senderQualityState ports 3012.
|
||||
func (c *Controller) senderQualityState(childPeerID string, edge *CommittedEdge, nowMs int64) SenderQualityState {
|
||||
if edge.Kind != UpstreamPeer {
|
||||
if c.sourceUsableForQuality(edge) {
|
||||
@@ -554,7 +552,6 @@ func (c *Controller) senderQualityState(childPeerID string, edge *CommittedEdge,
|
||||
return SenderQualityUnknown
|
||||
}
|
||||
|
||||
// currentQualityOperationSource ports 3034.
|
||||
func (c *Controller) currentQualityOperationSource(childPeerID string) *senderQualityIdentity {
|
||||
child, _ := c.participants.Get(childPeerID)
|
||||
edge, _ := c.upstreamByViewer.Get(childPeerID)
|
||||
@@ -578,7 +575,6 @@ func (c *Controller) currentQualityOperationSource(childPeerID string) *senderQu
|
||||
}
|
||||
}
|
||||
|
||||
// currentQualityOperationObservation ports 3061.
|
||||
func (c *Controller) currentQualityOperationObservation(op *operation) *senderQualityObservation {
|
||||
source := op.qualitySource
|
||||
current := c.currentQualityOperationSource(op.childPeerID)
|
||||
@@ -588,7 +584,6 @@ func (c *Controller) currentQualityOperationObservation(op *operation) *senderQu
|
||||
return c.senderQualityObservations[op.childPeerID]
|
||||
}
|
||||
|
||||
// senderQualityRegenerationBlocked ports 3080.
|
||||
func (c *Controller) senderQualityRegenerationBlocked(childPeerID string, edge *CommittedEdge) bool {
|
||||
block, ok := c.senderQualityRegenerationBlocks[childPeerID]
|
||||
if !ok {
|
||||
@@ -609,7 +604,6 @@ func (c *Controller) senderQualityRegenerationBlocked(childPeerID string, edge *
|
||||
return true
|
||||
}
|
||||
|
||||
// candidateSenderPersistentlyDegraded ports 3105.
|
||||
func (c *Controller) candidateSenderPersistentlyDegraded(att *attempt, nowMs int64) bool {
|
||||
return att.senderQualityState == SenderQualityDegraded &&
|
||||
att.senderQualityAcceptedAtMs != nil &&
|
||||
@@ -617,7 +611,6 @@ func (c *Controller) candidateSenderPersistentlyDegraded(att *attempt, nowMs int
|
||||
att.senderQualityConsecutiveDegradedWindows >= persistentDegradedWindows
|
||||
}
|
||||
|
||||
// sfuPublisherQualityState ports 3120.
|
||||
func (c *Controller) sfuPublisherQualityState(publicationGeneration string, nowMs int64) SenderQualityState {
|
||||
observation := c.sfuPublisherQualityObservation
|
||||
if observation != nil &&
|
||||
@@ -632,7 +625,6 @@ func (c *Controller) sfuPublisherQualityState(publicationGeneration string, nowM
|
||||
return SenderQualityUnknown
|
||||
}
|
||||
|
||||
// qualitySourcePathHealthy ports 3137.
|
||||
func (c *Controller) qualitySourcePathHealthy(peerID string, nowMs int64) bool {
|
||||
if peerID == c.hostPeerID {
|
||||
return true
|
||||
@@ -662,7 +654,6 @@ func (c *Controller) qualitySourcePathHealthy(peerID string, nowMs int64) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// decodedProgressFresh ports 3169.
|
||||
func (c *Controller) decodedProgressFresh(childPeerID string, edge *CommittedEdge, nowMs int64) bool {
|
||||
observation := c.qualityObservations[childPeerID]
|
||||
current, _ := c.participants.Get(childPeerID)
|
||||
@@ -676,8 +667,8 @@ func (c *Controller) decodedProgressFresh(childPeerID string, edge *CommittedEdg
|
||||
c.sourceUsableForQuality(edge)
|
||||
}
|
||||
|
||||
// buildQualityCandidates ports 3190: clear peers, remaining peers, SFU
|
||||
// (only under Host fanout relief), same-parent regeneration (§4.4 #36),
|
||||
// buildQualityCandidates orders clear peers, remaining peers, SFU
|
||||
// (only under Host fanout relief), same-parent regeneration,
|
||||
// filtered by the per-observation consumed ledger.
|
||||
func (c *Controller) buildQualityCandidates(childPeerID string, nowMs int64) []CandidatePlan {
|
||||
current, _ := c.upstreamByViewer.Get(childPeerID)
|
||||
@@ -736,7 +727,6 @@ func (c *Controller) buildQualityCandidates(childPeerID string, nowMs int64) []C
|
||||
return out
|
||||
}
|
||||
|
||||
// selectQualityChild ports 3251.
|
||||
func (c *Controller) selectQualityChild(nowMs int64) string {
|
||||
if !c.qualityConvergenceEnabled {
|
||||
return ""
|
||||
@@ -763,7 +753,6 @@ func (c *Controller) selectQualityChild(nowMs int64) string {
|
||||
return targets[0].peerID
|
||||
}
|
||||
|
||||
// senderQualityPersistentlyDegraded ports 3387.
|
||||
func (c *Controller) senderQualityPersistentlyDegraded(childPeerID string, edge *CommittedEdge, nowMs int64) bool {
|
||||
observation := c.senderQualityObservations[childPeerID]
|
||||
return c.senderQualityState(childPeerID, edge, nowMs) == SenderQualityDegraded &&
|
||||
@@ -771,7 +760,6 @@ func (c *Controller) senderQualityPersistentlyDegraded(childPeerID string, edge
|
||||
observation.consecutiveDegradedWindows >= persistentDegradedWindows
|
||||
}
|
||||
|
||||
// qualityOperationStillEligible ports 3401.
|
||||
func (c *Controller) qualityOperationStillEligible(op *operation, nowMs int64) bool {
|
||||
currentEdge, _ := c.upstreamByViewer.Get(op.childPeerID)
|
||||
observation := c.currentQualityOperationObservation(op)
|
||||
@@ -787,7 +775,6 @@ func (c *Controller) qualityOperationStillEligible(op *operation, nowMs int64) b
|
||||
(state == SenderQualityDegraded && c.hostFanoutNeedsSfuRelief(nowMs))
|
||||
}
|
||||
|
||||
// hostFanoutNeedsSfuRelief ports 3431.
|
||||
func (c *Controller) hostFanoutNeedsSfuRelief(nowMs int64) bool {
|
||||
count := 0
|
||||
for childPeerID, edge := range c.upstreamByViewer.All() {
|
||||
@@ -802,7 +789,6 @@ func (c *Controller) hostFanoutNeedsSfuRelief(nowMs int64) bool {
|
||||
return count >= 2
|
||||
}
|
||||
|
||||
// qualityUpstreamMatches ports 4506.
|
||||
func qualityUpstreamMatches(edge *CommittedEdge, upstream QualityUpstream) bool {
|
||||
if edge.Kind == UpstreamPeer {
|
||||
return upstream.Kind == UpstreamPeer && upstream.PeerID == edge.ParentPeerID
|
||||
@@ -810,7 +796,7 @@ func qualityUpstreamMatches(edge *CommittedEdge, upstream QualityUpstream) bool
|
||||
return upstream.Kind == UpstreamSfu
|
||||
}
|
||||
|
||||
// qualityObservationMatchesEdge ports 4515; upstreamPeerID "" is TS null.
|
||||
// qualityObservationMatchesEdge treats an empty upstreamPeerID as TypeScript null.
|
||||
func qualityObservationMatchesEdge(observation *routeQualityObservation, edge *CommittedEdge) bool {
|
||||
if edge.Kind == UpstreamPeer {
|
||||
return observation.upstreamKind == UpstreamPeer && observation.upstreamPeerID == edge.ParentPeerID
|
||||
@@ -818,7 +804,6 @@ func qualityObservationMatchesEdge(observation *routeQualityObservation, edge *C
|
||||
return observation.upstreamKind == UpstreamSfu && observation.upstreamPeerID == ""
|
||||
}
|
||||
|
||||
// sourceUsableForQuality ports 4526.
|
||||
func (c *Controller) sourceUsableForQuality(edge *CommittedEdge) bool {
|
||||
if edge.Kind == UpstreamPeer {
|
||||
parent, _ := c.participants.Get(edge.ParentPeerID)
|
||||
@@ -829,8 +814,8 @@ func (c *Controller) sourceUsableForQuality(edge *CommittedEdge) bool {
|
||||
c.hostPublication.Generation == edge.PublicationGeneration
|
||||
}
|
||||
|
||||
// clearQualityForParticipant ports 4541: the Host clears every child, a
|
||||
// viewer clears its subtree plus itself (§4.2 #21, §4.5 #50).
|
||||
// clearQualityForParticipant clears every child for the Host; a
|
||||
// viewer clears its subtree plus itself.
|
||||
func (c *Controller) clearQualityForParticipant(peerID string) {
|
||||
var affected []string
|
||||
if peerID == c.hostPeerID {
|
||||
@@ -853,7 +838,7 @@ func (c *Controller) clearQualityForParticipant(peerID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// requireSenderQualityBaseline ports 4609; a missing baseline reads as
|
||||
// In requireSenderQualityBaseline, a missing baseline reads as
|
||||
// false (`get(...) === true`).
|
||||
func (c *Controller) requireSenderQualityBaseline(peerID string, suppressFact bool) {
|
||||
for _, childPeerID := range withSelf(c.descendantsOf(peerID), peerID) {
|
||||
@@ -864,7 +849,6 @@ func (c *Controller) requireSenderQualityBaseline(peerID string, suppressFact bo
|
||||
}
|
||||
}
|
||||
|
||||
// clearSfuQuality ports 4624.
|
||||
func (c *Controller) clearSfuQuality() {
|
||||
c.sfuPublisherQualityObservation = nil
|
||||
for childPeerID, edge := range c.upstreamByViewer.All() {
|
||||
|
||||
@@ -8,7 +8,7 @@ type rootConvergencePick struct {
|
||||
plan CandidatePlan
|
||||
}
|
||||
|
||||
// stageRootConvergence ports 3272: staged from commitAttempt whenever a
|
||||
// stageRootConvergence is called from commitAttempt whenever a
|
||||
// child newly became a direct child of the Host.
|
||||
func (c *Controller) stageRootConvergence(rootPeerID string) {
|
||||
if !c.qualityConvergenceEnabled {
|
||||
@@ -25,7 +25,7 @@ func (c *Controller) stageRootConvergence(rootPeerID string) {
|
||||
"child", c.debugPeer(convergence.childPeerID))
|
||||
}
|
||||
|
||||
// takeRootConvergenceIntent ports 3284: one-shot (take = clear).
|
||||
// takeRootConvergenceIntent consumes the intent by clearing it.
|
||||
func (c *Controller) takeRootConvergenceIntent() *rootConvergencePick {
|
||||
rootPeerID := c.rootConvergenceRootPeerID
|
||||
c.rootConvergenceRootPeerID = ""
|
||||
@@ -45,8 +45,8 @@ type rootConvergence struct {
|
||||
plan CandidatePlan
|
||||
}
|
||||
|
||||
// rootConvergencePlan ports 3299: the busiest other Host root donates its
|
||||
// newest active direct child to the fresh root (§4.5 #45).
|
||||
// rootConvergencePlan makes the busiest other Host root donate its
|
||||
// newest active direct child to the fresh root.
|
||||
func (c *Controller) rootConvergencePlan(rootPeerID string) *rootConvergence {
|
||||
root, _ := c.participants.Get(rootPeerID)
|
||||
if root == nil || root.sessionID == "" ||
|
||||
@@ -92,7 +92,6 @@ func (c *Controller) rootConvergencePlan(rootPeerID string) *rootConvergence {
|
||||
return nil
|
||||
}
|
||||
|
||||
// rootConvergenceOperationStillEligible ports 3350.
|
||||
func (c *Controller) rootConvergenceOperationStillEligible(op *operation) bool {
|
||||
if op.reason != DemandRootConvergence {
|
||||
return true
|
||||
@@ -110,7 +109,6 @@ func (c *Controller) rootConvergenceOperationStillEligible(op *operation) bool {
|
||||
len(c.activeDirectChildren(current.ParentPeerID)) >= 2
|
||||
}
|
||||
|
||||
// isActiveHostRoot ports 3370.
|
||||
func (c *Controller) isActiveHostRoot(peerID string) bool {
|
||||
edge, _ := c.upstreamByViewer.Get(peerID)
|
||||
return edge != nil && edge.Kind == UpstreamPeer &&
|
||||
@@ -119,7 +117,6 @@ func (c *Controller) isActiveHostRoot(peerID string) bool {
|
||||
edge.PhysicalActive
|
||||
}
|
||||
|
||||
// activeDirectChildren ports 3380.
|
||||
func (c *Controller) activeDirectChildren(parentPeerID string) []string {
|
||||
children := []string{}
|
||||
for _, childPeerID := range c.childrenOf(parentPeerID) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// peekSfuBootstrapCarrier ports 3597.
|
||||
func (c *Controller) peekSfuBootstrapCarrier() *sfuBootstrapCarrier {
|
||||
intent := c.currentSfuBootstrapIntent()
|
||||
if intent == nil {
|
||||
@@ -22,7 +21,6 @@ func (c *Controller) peekSfuBootstrapCarrier() *sfuBootstrapCarrier {
|
||||
}
|
||||
}
|
||||
|
||||
// hasSfuBootstrapWork ports 3612.
|
||||
func (c *Controller) hasSfuBootstrapWork() bool {
|
||||
if c.currentSfuBootstrapIntent() != nil {
|
||||
return true
|
||||
@@ -37,7 +35,6 @@ func (c *Controller) hasSfuBootstrapWork() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// takeSfuBootstrapCarrier ports 3623.
|
||||
func (c *Controller) takeSfuBootstrapCarrier(nowMs int64, failedPeerIDs *[]string) *sfuBootstrapCarrier {
|
||||
if !c.sfuBootstrapGloballyNeeded() {
|
||||
return nil
|
||||
@@ -50,9 +47,9 @@ func (c *Controller) takeSfuBootstrapCarrier(nowMs int64, failedPeerIDs *[]strin
|
||||
return nil
|
||||
}
|
||||
|
||||
// currentSfuBootstrapIntent ports 3635: the sticky intent is revalidated on
|
||||
// every access and otherwise re-derived from the first exhausted viewer in
|
||||
// availableViewers order (§4.5 #43).
|
||||
// currentSfuBootstrapIntent revalidates the sticky intent on
|
||||
// every access and otherwise re-derives it from the first exhausted viewer in
|
||||
// availableViewers order.
|
||||
func (c *Controller) currentSfuBootstrapIntent() *sfuBootstrapIntent {
|
||||
if current := c.sfuBootstrapIntent; current != nil {
|
||||
demand, _ := c.participants.Get(current.demandPeerID)
|
||||
@@ -78,7 +75,7 @@ func (c *Controller) currentSfuBootstrapIntent() *sfuBootstrapIntent {
|
||||
return nil
|
||||
}
|
||||
|
||||
// bootstrapOperationOwned ports 3663; checked first in validateOrAdvance
|
||||
// bootstrapOperationOwned is checked first in validateOrAdvance
|
||||
// so a bootstrap operation evaporates with its intent.
|
||||
func (c *Controller) bootstrapOperationOwned(op *operation) bool {
|
||||
if op.reason != DemandSfuBootstrap {
|
||||
@@ -94,7 +91,6 @@ func (c *Controller) bootstrapOperationOwned(op *operation) bool {
|
||||
return slices.Contains(c.safeSfuBootstrapCarriers(op.demandPeerID), op.childPeerID)
|
||||
}
|
||||
|
||||
// sfuBootstrapNeeded ports 3681.
|
||||
func (c *Controller) sfuBootstrapNeeded(demandPeerID string) bool {
|
||||
if !c.sfuBootstrapGloballyNeeded() {
|
||||
return false
|
||||
@@ -105,7 +101,6 @@ func (c *Controller) sfuBootstrapNeeded(demandPeerID string) bool {
|
||||
c.sfuOpportunityAvailable(demandPeerID)
|
||||
}
|
||||
|
||||
// sfuOpportunityAvailable ports 3690.
|
||||
func (c *Controller) sfuOpportunityAvailable(demandPeerID string) bool {
|
||||
return c.candidateOpportunityAvailable(demandPeerID, CandidatePlan{
|
||||
Tuple: CandidateTuple{Kind: UpstreamSfu, Publication: PublicationReuse},
|
||||
@@ -113,12 +108,10 @@ func (c *Controller) sfuOpportunityAvailable(demandPeerID string) bool {
|
||||
})
|
||||
}
|
||||
|
||||
// sfuBootstrapGloballyNeeded ports 3697.
|
||||
func (c *Controller) sfuBootstrapGloballyNeeded() bool {
|
||||
return c.sfuEnabled && c.hostPublication == nil && !c.hostHasPublicationSlot()
|
||||
}
|
||||
|
||||
// safeSfuBootstrapCarriers ports 3705.
|
||||
func (c *Controller) safeSfuBootstrapCarriers(demandPeerID string) []string {
|
||||
if !c.sfuBootstrapNeeded(demandPeerID) {
|
||||
return []string{}
|
||||
@@ -126,8 +119,8 @@ func (c *Controller) safeSfuBootstrapCarriers(demandPeerID string) []string {
|
||||
return c.sfuBootstrapCarriers()
|
||||
}
|
||||
|
||||
// sfuBootstrapCarriers ports 3712: the Host's usable direct children that
|
||||
// can create a publication, newest first (§4.5 #44).
|
||||
// sfuBootstrapCarriers returns the Host's usable direct children that
|
||||
// can create a publication, newest first.
|
||||
func (c *Controller) sfuBootstrapCarriers() []string {
|
||||
tuple := CandidateTuple{Kind: UpstreamSfu, Publication: PublicationCreate}
|
||||
carriers := []string{}
|
||||
@@ -154,25 +147,22 @@ func (c *Controller) sfuBootstrapCarriers() []string {
|
||||
return carriers
|
||||
}
|
||||
|
||||
// sfuBootstrapOpportunityAvailable ports 3744.
|
||||
func (c *Controller) sfuBootstrapOpportunityAvailable(carrierPeerID string, plan CandidatePlan) bool {
|
||||
consumed, ok := c.consumedSfuBootstrapOpportunities.Get(c.sfuBootstrapOpportunityBase(carrierPeerID, plan))
|
||||
return !ok || endpointTransitionRank(plan.EndpointTransition) < consumed
|
||||
}
|
||||
|
||||
// sfuBootstrapOpportunityBase ports 3757.
|
||||
func (c *Controller) sfuBootstrapOpportunityBase(carrierPeerID string, plan CandidatePlan) string {
|
||||
return carrierPeerID + "\x00" + c.candidateOpportunityBase(carrierPeerID, plan)
|
||||
}
|
||||
|
||||
// consumeSfuBootstrapOpportunity ports 3767.
|
||||
func (c *Controller) consumeSfuBootstrapOpportunity(carrierPeerID string, plan CandidatePlan) {
|
||||
consumeOpportunity(&c.consumedSfuBootstrapOpportunities,
|
||||
c.sfuBootstrapOpportunityBase(carrierPeerID, plan),
|
||||
endpointTransitionRank(plan.EndpointTransition))
|
||||
}
|
||||
|
||||
// completeSfuBootstrapCarrier ports 3778: consumes the carrier's attempt
|
||||
// completeSfuBootstrapCarrier consumes the carrier's attempt
|
||||
// and returns the failed demand peer only once no carrier is left.
|
||||
func (c *Controller) completeSfuBootstrapCarrier(carrierPeerID string, nowMs int64, bucket RejectionBucket, attemptedPlan *CandidatePlan) string {
|
||||
intent := c.sfuBootstrapIntent
|
||||
@@ -202,7 +192,7 @@ func (c *Controller) completeSfuBootstrapCarrier(carrierPeerID string, nowMs int
|
||||
return intent.demandPeerID
|
||||
}
|
||||
|
||||
// reportSfuBootstrapFailures ports 3808 (availableViewers order, §4.5 #43).
|
||||
// reportSfuBootstrapFailures follows availableViewers order.
|
||||
func (c *Controller) reportSfuBootstrapFailures(nowMs int64, failedPeerIDs *[]string) {
|
||||
for _, demand := range c.availableViewers(true) {
|
||||
if !demand.availabilityExhausted ||
|
||||
@@ -217,7 +207,6 @@ func (c *Controller) reportSfuBootstrapFailures(nowMs int64, failedPeerIDs *[]st
|
||||
}
|
||||
}
|
||||
|
||||
// rememberUnavailableSfuBootstrap ports 3827.
|
||||
func (c *Controller) rememberUnavailableSfuBootstrap(demandPeerID string) {
|
||||
if !c.sfuBootstrapNeeded(demandPeerID) {
|
||||
return
|
||||
@@ -230,7 +219,6 @@ func (c *Controller) rememberUnavailableSfuBootstrap(demandPeerID string) {
|
||||
c.sfuBootstrapIntent = nil
|
||||
}
|
||||
|
||||
// stageSfuBootstrap ports 3835.
|
||||
func (c *Controller) stageSfuBootstrap(op *operation) {
|
||||
demand, _ := c.participants.Get(op.demandPeerID)
|
||||
if demand == nil || demand.sessionID == "" || !c.sfuBootstrapNeeded(demand.peerID) {
|
||||
@@ -247,7 +235,6 @@ func (c *Controller) stageSfuBootstrap(op *operation) {
|
||||
c.sfuBootstrapIntent = &sfuBootstrapIntent{demandPeerID: demand.peerID, demandSessionID: demand.sessionID}
|
||||
}
|
||||
|
||||
// clearSfuBootstrapForDemand ports 3854.
|
||||
func (c *Controller) clearSfuBootstrapForDemand(demandPeerID string) {
|
||||
if demand, ok := c.participants.Get(demandPeerID); ok {
|
||||
demand.sfuFirstAtNextRoute = false
|
||||
@@ -257,7 +244,6 @@ func (c *Controller) clearSfuBootstrapForDemand(demandPeerID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// bootstrapCandidateAvailable ports 4334.
|
||||
func (c *Controller) bootstrapCandidateAvailable(demandPeerID string) bool {
|
||||
return len(c.safeSfuBootstrapCarriers(demandPeerID)) > 0
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package route
|
||||
|
||||
import "github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
||||
|
||||
// recordDemand ports 4633: replaces the peer's timing record wholesale.
|
||||
// recordDemand replaces the peer's timing record wholesale.
|
||||
func (c *Controller) recordDemand(childPeerID string, nowMs int64, reason DemandReason) {
|
||||
c.routeTimings[childPeerID] = &routeTimingRecord{
|
||||
demandAtMs: nowMs,
|
||||
@@ -12,14 +12,12 @@ func (c *Controller) recordDemand(childPeerID string, nowMs int64, reason Demand
|
||||
}
|
||||
}
|
||||
|
||||
// ensureDemand ports 4647.
|
||||
func (c *Controller) ensureDemand(childPeerID string, nowMs int64, reason DemandReason) {
|
||||
if _, ok := c.routeTimings[childPeerID]; !ok {
|
||||
c.recordDemand(childPeerID, nowMs, reason)
|
||||
}
|
||||
}
|
||||
|
||||
// startOperationTiming ports 4657.
|
||||
func (c *Controller) startOperationTiming(childPeerID string, nowMs int64) {
|
||||
record := c.routeTimings[childPeerID]
|
||||
if record == nil {
|
||||
@@ -34,7 +32,6 @@ func (c *Controller) startOperationTiming(childPeerID string, nowMs int64) {
|
||||
record.rejectionBucket = RejectionNone
|
||||
}
|
||||
|
||||
// startCandidateTiming ports 4668.
|
||||
func (c *Controller) startCandidateTiming(childPeerID string, nowMs int64) {
|
||||
record := c.routeTimings[childPeerID]
|
||||
if record == nil {
|
||||
@@ -48,14 +45,12 @@ func (c *Controller) startCandidateTiming(childPeerID string, nowMs int64) {
|
||||
record.rejectionBucket = RejectionNone
|
||||
}
|
||||
|
||||
// noteRejection ports 4678.
|
||||
func (c *Controller) noteRejection(childPeerID string, bucket RejectionBucket) {
|
||||
if record := c.routeTimings[childPeerID]; record != nil {
|
||||
record.rejectionBucket = bucket
|
||||
}
|
||||
}
|
||||
|
||||
// clearCandidateTiming ports 4686.
|
||||
func (c *Controller) clearCandidateTiming(childPeerID string) {
|
||||
record := c.routeTimings[childPeerID]
|
||||
if record == nil {
|
||||
@@ -67,7 +62,6 @@ func (c *Controller) clearCandidateTiming(childPeerID string) {
|
||||
record.finalRoute = FinalRouteWaiting
|
||||
}
|
||||
|
||||
// finishTiming ports 4695.
|
||||
func (c *Controller) finishTiming(childPeerID string, nowMs int64, finalRoute FinalRoute, rejectionBucket RejectionBucket, firstDecodedFrame bool) {
|
||||
record := c.routeTimings[childPeerID]
|
||||
if record == nil {
|
||||
@@ -83,12 +77,10 @@ func (c *Controller) finishTiming(childPeerID string, nowMs int64, finalRoute Fi
|
||||
record.rejectionBucket = rejectionBucket
|
||||
}
|
||||
|
||||
// usableRoute ports 4710.
|
||||
func (c *Controller) usableRoute(childPeerID string) bool {
|
||||
return c.currentFinalRoute(childPeerID) != FinalRouteWaiting
|
||||
}
|
||||
|
||||
// currentFinalRoute ports 4714.
|
||||
func (c *Controller) currentFinalRoute(childPeerID string) FinalRoute {
|
||||
edge, _ := c.upstreamByViewer.Get(childPeerID)
|
||||
if edge == nil || !edge.Usable || !edge.PhysicalActive {
|
||||
@@ -108,7 +100,6 @@ func (c *Controller) currentFinalRoute(childPeerID string) FinalRoute {
|
||||
return FinalRouteWaiting
|
||||
}
|
||||
|
||||
// routeDemandReason ports 4729.
|
||||
func (c *Controller) routeDemandReason(childPeerID string) DemandReason {
|
||||
edge, _ := c.upstreamByViewer.Get(childPeerID)
|
||||
if edge == nil {
|
||||
@@ -127,7 +118,6 @@ func (c *Controller) routeDemandReason(childPeerID string) DemandReason {
|
||||
return DemandEdgeUnavailable
|
||||
}
|
||||
|
||||
// diagnosticParent ports 4448.
|
||||
func (c *Controller) diagnosticParent(edge *CommittedEdge, ordinals map[string]int) protocol.RouteDiagnosticParent {
|
||||
if edge == nil || !edge.Usable || !edge.PhysicalActive {
|
||||
return protocol.RouteDiagnosticParent{Kind: "none"}
|
||||
@@ -150,7 +140,7 @@ func (c *Controller) diagnosticParent(edge *CommittedEdge, ordinals map[string]i
|
||||
return protocol.RouteDiagnosticParent{Kind: "viewer", Ordinal: protocol.Int(ordinal)}
|
||||
}
|
||||
|
||||
// diagnosticQuality ports 4471: nil (TS null) unless the observation is
|
||||
// diagnosticQuality returns nil (TS null) unless the observation is
|
||||
// fresh, non-empty and still describes the current edge.
|
||||
func (c *Controller) diagnosticQuality(childPeerID string, edge *CommittedEdge, nowMs int64) *protocol.RouteDiagnosticQuality {
|
||||
observation := c.qualityObservations[childPeerID]
|
||||
@@ -180,7 +170,7 @@ func (c *Controller) diagnosticQuality(childPeerID string, edge *CommittedEdge,
|
||||
}
|
||||
}
|
||||
|
||||
// elapsedMs ports 4964: nil (TS null) when the end precedes the start or
|
||||
// elapsedMs returns nil (TS null) when the end precedes the start or
|
||||
// the difference is not a safe integer; the inputs are integral so
|
||||
// Math.floor is the identity.
|
||||
func elapsedMs(startedAtMs, endedAtMs int64) *protocol.Int {
|
||||
|
||||
@@ -88,7 +88,6 @@ func NewAdmission(options AdmissionOptions) *Admission {
|
||||
}
|
||||
}
|
||||
|
||||
// ReservePublication ports reservePublication.
|
||||
func (admission *Admission) ReservePublication(fence ResourceFence) bool {
|
||||
assertFence(fence)
|
||||
room, _ := admission.rooms.Get(fence.RoomID)
|
||||
@@ -124,7 +123,6 @@ func (admission *Admission) ReservePublication(fence ResourceFence) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ReserveSubscription ports reserveSubscription.
|
||||
func (admission *Admission) ReserveSubscription(fence SubscriptionFence) bool {
|
||||
assertSubscriptionFence(fence)
|
||||
publication := admission.publication(fence.ResourceFence)
|
||||
@@ -147,8 +145,8 @@ func (admission *Admission) ReserveSubscription(fence SubscriptionFence) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// CommitPublication ports commitPublication. The second result is false where
|
||||
// the TypeScript returns null; an empty slice with true is its empty array.
|
||||
// CommitPublication returns false as its second result where TypeScript returns
|
||||
// null; an empty slice with true is its empty array.
|
||||
func (admission *Admission) CommitPublication(fence ResourceFence) ([]ResourceFence, bool) {
|
||||
assertFence(fence)
|
||||
room, _ := admission.rooms.Get(fence.RoomID)
|
||||
@@ -193,7 +191,6 @@ func (admission *Admission) CommitPublication(fence ResourceFence) ([]ResourceFe
|
||||
return draining, true
|
||||
}
|
||||
|
||||
// CommitSubscription ports commitSubscription.
|
||||
func (admission *Admission) CommitSubscription(fence SubscriptionFence) bool {
|
||||
assertSubscriptionFence(fence)
|
||||
publication := admission.publication(fence.ResourceFence)
|
||||
@@ -214,7 +211,6 @@ func (admission *Admission) CommitSubscription(fence SubscriptionFence) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// BeginSubscriptionDrain ports beginSubscriptionDrain.
|
||||
func (admission *Admission) BeginSubscriptionDrain(fence SubscriptionFence) bool {
|
||||
assertSubscriptionFence(fence)
|
||||
publication := admission.publication(fence.ResourceFence)
|
||||
@@ -229,7 +225,6 @@ func (admission *Admission) BeginSubscriptionDrain(fence SubscriptionFence) bool
|
||||
return true
|
||||
}
|
||||
|
||||
// BeginDrain ports beginDrain.
|
||||
func (admission *Admission) BeginDrain(fence ResourceFence) bool {
|
||||
assertFence(fence)
|
||||
publication := admission.publication(fence)
|
||||
@@ -270,7 +265,6 @@ func (admission *Admission) CompleteSubscriptionDrain(fence SubscriptionFence) b
|
||||
return true
|
||||
}
|
||||
|
||||
// CompleteDrain ports completeDrain.
|
||||
func (admission *Admission) CompleteDrain(fence ResourceFence) bool {
|
||||
assertFence(fence)
|
||||
room, _ := admission.rooms.Get(fence.RoomID)
|
||||
@@ -294,7 +288,6 @@ func (admission *Admission) CompleteDrain(fence ResourceFence) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// BeginDrainRoom ports beginDrainRoom.
|
||||
func (admission *Admission) BeginDrainRoom(roomID string) []ResourceFence {
|
||||
if roomID == "" {
|
||||
panic("SFU resource room ID is invalid")
|
||||
@@ -313,7 +306,6 @@ func (admission *Admission) BeginDrainRoom(roomID string) []ResourceFence {
|
||||
return fences
|
||||
}
|
||||
|
||||
// BeginDrainAll ports beginDrainAll.
|
||||
func (admission *Admission) BeginDrainAll() []ResourceFence {
|
||||
var fences []ResourceFence
|
||||
for _, roomID := range admission.rooms.Keys() {
|
||||
@@ -322,7 +314,6 @@ func (admission *Admission) BeginDrainAll() []ResourceFence {
|
||||
return fences
|
||||
}
|
||||
|
||||
// Usage ports usage().
|
||||
func (admission *Admission) Usage() Usage {
|
||||
return Usage{Ingress: admission.ingressInUse, Egress: admission.egressInUse}
|
||||
}
|
||||
@@ -375,8 +366,7 @@ func assertSubscriptionFence(fence SubscriptionFence) {
|
||||
}
|
||||
}
|
||||
|
||||
// assertPositiveSafeInteger ports assertPositiveSafeInteger. Go's int makes the
|
||||
// TypeScript Number.isSafeInteger fractional case unrepresentable; only the
|
||||
// Go's int makes the Number.isSafeInteger fractional case unrepresentable; only the
|
||||
// range checks survive.
|
||||
func assertPositiveSafeInteger(value int, name string) {
|
||||
if value <= 0 || int64(value) > protocol.MaxSafeInteger {
|
||||
|
||||
@@ -19,10 +19,10 @@ import (
|
||||
// the same expression).
|
||||
func viewerKey(roomID, peerID string) string { return roomID + ":" + peerID }
|
||||
|
||||
// sendViewerPresence ports sendViewerPresence (O12): the host entry first
|
||||
// sendViewerPresence lists the host entry first
|
||||
// when it has a display name, then viewers in store order; recipients are
|
||||
// the opted-in host first, then opted-in viewers in store order, deduped by
|
||||
// connection (hazard 11).
|
||||
// connection.
|
||||
func (s *Server) sendViewerPresence(roomID string) {
|
||||
if _, deferred := s.deferredViewerPresenceRooms[roomID]; deferred {
|
||||
return
|
||||
@@ -108,7 +108,6 @@ func (s *Server) sendViewerPresence(roomID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// broadcastHostStatus ports broadcastHostStatus (O10).
|
||||
func (s *Server) broadcastHostStatus(roomID string, online, paused bool) {
|
||||
for _, viewer := range s.store.GetConnectedViewers(roomID) {
|
||||
s.sendToSession(viewer.SessionID, protocol.HostStatusMessage{
|
||||
@@ -119,8 +118,8 @@ func (s *Server) broadcastHostStatus(roomID string, online, paused bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleDisconnect ports handleDisconnect. It runs once, from the reader
|
||||
// goroutine's exit (D6).
|
||||
// handleDisconnect runs once, from the reader
|
||||
// goroutine's exit.
|
||||
func (s *Server) handleDisconnect(sess *session) {
|
||||
if !s.sessions.Has(sess) {
|
||||
return
|
||||
@@ -159,7 +158,7 @@ func (s *Server) handleDisconnect(sess *session) {
|
||||
|
||||
s.sendViewerPresence(disconnected.RoomID)
|
||||
|
||||
// T5: the viewer grace timer. The registered-pointer check stands in
|
||||
// The viewer grace timer. The registered-pointer check stands in
|
||||
// for clearTimeout; the store's removeDisconnectedViewer is the identity
|
||||
// guard against a viewer that reconnected meanwhile.
|
||||
roomID, peerID := disconnected.RoomID, disconnected.PeerID
|
||||
@@ -180,8 +179,8 @@ func (s *Server) handleDisconnect(sess *session) {
|
||||
s.viewerGraceTimers[key] = timer
|
||||
}
|
||||
|
||||
// stopSharing ports stopSharing (O9: per viewer, sharing-stopped then
|
||||
// host-status). The share generation deliberately survives (hazard 2).
|
||||
// stopSharing sends sharing-stopped then host-status to each viewer.
|
||||
// The share generation deliberately survives.
|
||||
func (s *Server) stopSharing(roomID string) {
|
||||
if current, ok := s.shares[roomID]; ok {
|
||||
s.shares[roomID] = roomShare{generation: current.generation}
|
||||
@@ -198,7 +197,7 @@ func (s *Server) stopSharing(roomID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// abandonRoom ports abandonRoom; the store error is the caller's to map.
|
||||
// abandonRoom leaves store-error mapping to its caller.
|
||||
func (s *Server) abandonRoom(roomID string) error {
|
||||
abandoned, err := s.store.AbandonRoom(roomID)
|
||||
if err != nil {
|
||||
@@ -228,7 +227,7 @@ func (s *Server) closeRoom(closed room.ClosedRoom) {
|
||||
}
|
||||
}
|
||||
|
||||
// revokeGrantViewers ports revokeGrantViewers (O5): three separate passes
|
||||
// revokeGrantViewers makes three separate passes
|
||||
// over the revoked viewers, with presence deferred until the end.
|
||||
func (s *Server) revokeGrantViewers(roomID string, update room.ViewerGrantUpdate) {
|
||||
if len(update.RevokedViewers) == 0 {
|
||||
@@ -262,7 +261,7 @@ func (s *Server) revokeGrantViewers(roomID string, update room.ViewerGrantUpdate
|
||||
s.sendViewerPresence(roomID)
|
||||
}
|
||||
|
||||
// closeRevokedViewerSession ports closeRevokedViewerSession: deauthorize
|
||||
// closeRevokedViewerSession deauthorizes
|
||||
// before the close so queued callbacks of the old generation cannot route
|
||||
// signaling after the persistent authorization commit.
|
||||
func (s *Server) closeRevokedViewerSession(sessionID string) {
|
||||
@@ -280,7 +279,6 @@ func (s *Server) closeRevokedViewerSession(sessionID string) {
|
||||
sess.close(websocket.StatusCode(protocol.SignalCloseViewerAccessRevoked), "Viewer access revoked")
|
||||
}
|
||||
|
||||
// clearViewerGrace ports clearViewerGrace.
|
||||
func (s *Server) clearViewerGrace(roomID, peerID string) {
|
||||
key := viewerKey(roomID, peerID)
|
||||
if timer, ok := s.viewerGraceTimers[key]; ok {
|
||||
@@ -289,14 +287,12 @@ func (s *Server) clearViewerGrace(roomID, peerID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// clearViewerState ports clearViewerState.
|
||||
func (s *Server) clearViewerState(roomID, peerID string) {
|
||||
s.clearViewerGrace(roomID, peerID)
|
||||
s.clearViewerEvidence(roomID, peerID)
|
||||
}
|
||||
|
||||
// clearRoomGraceTimers ports clearRoomGraceTimers (O13: prefix scan; Go
|
||||
// maps allow deletion during range).
|
||||
// clearRoomGraceTimers scans by room prefix; Go maps allow deletion during range.
|
||||
func (s *Server) clearRoomGraceTimers(roomID string) {
|
||||
prefix := roomID + ":"
|
||||
for key, timer := range s.viewerGraceTimers {
|
||||
@@ -307,8 +303,6 @@ func (s *Server) clearRoomGraceTimers(roomID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// clearRoomViewerEvidence ports clearRoomConnectionIds (O13) without the
|
||||
// connection-ID mirror it also cleared.
|
||||
func (s *Server) clearRoomViewerEvidence(roomID string) {
|
||||
prefix := roomID + ":"
|
||||
for key := range s.viewerQualityEvidenceGates {
|
||||
|
||||
@@ -60,7 +60,7 @@ type sfuFallback struct {
|
||||
}
|
||||
|
||||
// routerHooks are the signaling server callbacks of HybridMediaRouterOptions.
|
||||
// Every hook is called with mu held and must not block (D11). shareGeneration
|
||||
// Every hook is called with mu held and must not block. shareGeneration
|
||||
// returns "" where the TS returned undefined; routesChanged may be nil.
|
||||
type routerHooks struct {
|
||||
sendToSession func(sessionID string, message protocol.ServerMessage)
|
||||
@@ -68,8 +68,8 @@ type routerHooks struct {
|
||||
routesChanged func(roomID string)
|
||||
}
|
||||
|
||||
// routerOptions is HybridMediaRouterOptions plus the shared mutex and the
|
||||
// timer factory of D5. now and afterFunc default to the wall clock.
|
||||
// routerOptions supplies the shared mutex and timer factory.
|
||||
// now and afterFunc default to the wall clock.
|
||||
type routerOptions struct {
|
||||
mu *sync.Mutex
|
||||
store *room.Store
|
||||
@@ -142,11 +142,11 @@ type peerSignalDebugInput struct {
|
||||
|
||||
// roomRuntime is RoomRuntime. pumping is the `pump` promise used as a
|
||||
// single-flight token; deadlineGeneration is bumped by every clearDeadline so
|
||||
// a callback that lost the Stop race sees it is stale (D5).
|
||||
// a callback that lost the Stop race sees it is stale.
|
||||
type roomRuntime struct {
|
||||
hostPeerID string
|
||||
controller *route.Controller
|
||||
// O32: capacities survive stopRoom in insertion order.
|
||||
// Capacities survive stopRoom in insertion order.
|
||||
advertisedCapacityByViewer ordered.Map[string, int]
|
||||
requested bool
|
||||
pumping bool
|
||||
@@ -164,7 +164,7 @@ type router struct {
|
||||
now func() int64
|
||||
afterFunc func(time.Duration, func()) func() bool
|
||||
|
||||
// O14, O16, O29: rooms, drain tasks and waiters are walked in insertion
|
||||
// Rooms, drain tasks and waiters are walked in insertion
|
||||
// order.
|
||||
rooms ordered.Map[string, *roomRuntime]
|
||||
resourceWaiters ordered.Map[string, struct{}]
|
||||
@@ -257,7 +257,6 @@ func (r *router) close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// connectParticipant ports connectParticipant.
|
||||
func (r *router) connectParticipant(input authenticatedRouteParticipant) hybridAuthenticationState {
|
||||
rm := r.room(input.roomID)
|
||||
if input.role == protocol.RoleHost {
|
||||
@@ -298,7 +297,6 @@ func (r *router) connectParticipant(input authenticatedRouteParticipant) hybridA
|
||||
return state
|
||||
}
|
||||
|
||||
// completeAuthentication ports completeAuthentication.
|
||||
func (r *router) completeAuthentication(participant authenticatedRouteParticipant, _ hybridAuthenticationState) {
|
||||
rm, _ := r.rooms.Get(participant.roomID)
|
||||
if rm == nil || rm.controller == nil {
|
||||
@@ -309,7 +307,6 @@ func (r *router) completeAuthentication(participant authenticatedRouteParticipan
|
||||
r.requestPump(participant.roomID)
|
||||
}
|
||||
|
||||
// setPaused ports setPaused.
|
||||
func (r *router) setPaused(roomID string, paused bool) {
|
||||
rm, _ := r.rooms.Get(roomID)
|
||||
if rm == nil || rm.controller == nil {
|
||||
@@ -325,7 +322,6 @@ func (r *router) setPaused(roomID string, paused bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// routeDiagnosticSnapshot ports routeDiagnosticSnapshot.
|
||||
func (r *router) routeDiagnosticSnapshot(roomID string) protocol.RouteDiagnosticSnapshot {
|
||||
if rm, _ := r.rooms.Get(roomID); rm != nil && rm.controller != nil {
|
||||
return rm.controller.RouteDiagnosticSnapshot(r.now())
|
||||
@@ -333,8 +329,6 @@ func (r *router) routeDiagnosticSnapshot(roomID string) protocol.RouteDiagnostic
|
||||
return protocol.RouteDiagnosticSnapshot{Children: []protocol.RouteDiagnosticChild{}, Operation: nil}
|
||||
}
|
||||
|
||||
// observeQualityEvidence ports observeQualityEvidence; roomID is the TS
|
||||
// input's roomId and evidence the rest.
|
||||
func (r *router) observeQualityEvidence(roomID string, evidence route.RouteQualityEvidenceInput) route.RouteQualityEvidenceResult {
|
||||
result := route.QualityEvidenceRejected
|
||||
if rm, _ := r.rooms.Get(roomID); rm != nil && rm.controller != nil {
|
||||
@@ -371,7 +365,6 @@ func (r *router) observeQualityEvidence(roomID string, evidence route.RouteQuali
|
||||
return result
|
||||
}
|
||||
|
||||
// observeSenderQualityEvidence ports observeSenderQualityEvidence.
|
||||
func (r *router) observeSenderQualityEvidence(participant authenticatedRouteParticipant, message protocol.SenderQualityEvidenceMessage) bool {
|
||||
rm, _ := r.rooms.Get(participant.roomID)
|
||||
if rm == nil || rm.controller == nil {
|
||||
@@ -446,7 +439,6 @@ func (r *router) observeSenderQualityEvidence(participant authenticatedRoutePart
|
||||
return result.Accepted
|
||||
}
|
||||
|
||||
// observeSfuPublisherQualityEvidence ports observeSfuPublisherQualityEvidence.
|
||||
func (r *router) observeSfuPublisherQualityEvidence(participant authenticatedRouteParticipant, message protocol.SfuPublisherQualityEvidenceMessage) bool {
|
||||
rm, _ := r.rooms.Get(participant.roomID)
|
||||
if rm == nil || rm.controller == nil || participant.role != protocol.RoleHost {
|
||||
@@ -513,7 +505,6 @@ func (r *router) observeSfuPublisherQualityEvidence(participant authenticatedRou
|
||||
return result.Accepted
|
||||
}
|
||||
|
||||
// resetSenderQuality ports resetSenderQuality.
|
||||
func (r *router) resetSenderQuality(participant authenticatedRouteParticipant) {
|
||||
rm, _ := r.rooms.Get(participant.roomID)
|
||||
if rm == nil || rm.controller == nil {
|
||||
@@ -527,14 +518,12 @@ func (r *router) resetSenderQuality(participant authenticatedRouteParticipant) {
|
||||
r.requestPump(participant.roomID)
|
||||
}
|
||||
|
||||
// isActivePeerParentOf ports isActivePeerParentOf.
|
||||
func (r *router) isActivePeerParentOf(roomID, parentPeerID, childPeerID string) bool {
|
||||
_, activeParent, ok := r.resolveActivePeerEdge(roomID, childPeerID)
|
||||
return ok && activeParent == parentPeerID
|
||||
}
|
||||
|
||||
// resolveActivePeerEdge ports resolveActivePeerEdge; ok is false where the
|
||||
// TS returned undefined.
|
||||
// resolveActivePeerEdge returns false when the child has no active Peer edge.
|
||||
func (r *router) resolveActivePeerEdge(roomID, childPeerID string) (revision int64, parentPeerID string, ok bool) {
|
||||
edge, found := r.resolveActiveViewerMediaEdge(roomID, childPeerID)
|
||||
if !found || edge.upstream.Kind != "peer" {
|
||||
@@ -543,7 +532,6 @@ func (r *router) resolveActivePeerEdge(roomID, childPeerID string) (revision int
|
||||
return edge.revision, edge.upstream.PeerID, true
|
||||
}
|
||||
|
||||
// resolveActiveViewerMediaEdge ports resolveActiveViewerMediaEdge.
|
||||
func (r *router) resolveActiveViewerMediaEdge(roomID, childPeerID string) (activeViewerMediaEdge, bool) {
|
||||
rm, _ := r.rooms.Get(roomID)
|
||||
if rm == nil || rm.controller == nil {
|
||||
@@ -565,7 +553,6 @@ func (r *router) resolveActiveViewerMediaEdge(roomID, childPeerID string) (activ
|
||||
}, true
|
||||
}
|
||||
|
||||
// getViewerRouteUpstream ports getViewerRouteUpstream.
|
||||
func (r *router) getViewerRouteUpstream(roomID, viewerPeerID string) protocol.MediaRouteUpstream {
|
||||
rm, _ := r.rooms.Get(roomID)
|
||||
if rm == nil || rm.controller == nil {
|
||||
@@ -665,7 +652,6 @@ func (r *router) peerSignalAuthorization(input peerSignalInput) signalAuthorizat
|
||||
return signalAuthorizationDenied
|
||||
}
|
||||
|
||||
// debugPeerSignal ports debugPeerSignal.
|
||||
func (r *router) debugPeerSignal(input peerSignalDebugInput) {
|
||||
if !routeDebugEnabled() {
|
||||
return
|
||||
@@ -690,7 +676,6 @@ func (r *router) debugPeerSignal(input peerSignalDebugInput) {
|
||||
"authorization", authorization)
|
||||
}
|
||||
|
||||
// setViewerRelayCapacity ports setViewerRelayCapacity.
|
||||
func (r *router) setViewerRelayCapacity(participant authenticatedRouteParticipant, downstreamEdges int) {
|
||||
rm := r.room(participant.roomID)
|
||||
r.debug(participant.roomID, "relay-capacity-received",
|
||||
@@ -703,7 +688,6 @@ func (r *router) setViewerRelayCapacity(participant authenticatedRouteParticipan
|
||||
}
|
||||
}
|
||||
|
||||
// handleRouteReady ports handleRouteReady.
|
||||
func (r *router) handleRouteReady(participant authenticatedRouteParticipant, message protocol.RouteReadyMessage) {
|
||||
if participant.role != protocol.RoleViewer || message.Phase != "prepare" {
|
||||
return
|
||||
@@ -748,7 +732,6 @@ func (r *router) handleRouteReady(participant authenticatedRouteParticipant, mes
|
||||
r.requestPump(participant.roomID)
|
||||
}
|
||||
|
||||
// handleRouteTransportConnected ports handleRouteTransportConnected.
|
||||
func (r *router) handleRouteTransportConnected(participant authenticatedRouteParticipant, message protocol.RouteTransportConnectedMessage) {
|
||||
if participant.role != protocol.RoleViewer {
|
||||
return
|
||||
@@ -786,7 +769,6 @@ func (r *router) handleRouteTransportConnected(participant authenticatedRoutePar
|
||||
}
|
||||
}
|
||||
|
||||
// handleRouteMediaUnavailable ports handleRouteMediaUnavailable.
|
||||
func (r *router) handleRouteMediaUnavailable(participant authenticatedRouteParticipant, message protocol.RouteMediaUnavailableMessage) {
|
||||
if participant.role != protocol.RoleViewer {
|
||||
return
|
||||
@@ -815,7 +797,6 @@ func (r *router) handleRouteMediaUnavailable(participant authenticatedRouteParti
|
||||
r.requestPump(participant.roomID)
|
||||
}
|
||||
|
||||
// handleRouteFailed ports handleRouteFailed.
|
||||
func (r *router) handleRouteFailed(participant authenticatedRouteParticipant, message protocol.RouteFailedMessage) {
|
||||
connectionID := ""
|
||||
if message.ConnectionID != nil {
|
||||
@@ -939,7 +920,6 @@ func (r *router) refreshSfu(participant authenticatedRouteParticipant, revision
|
||||
}
|
||||
}
|
||||
|
||||
// disconnectParticipant ports disconnectParticipant.
|
||||
func (r *router) disconnectParticipant(roomID, peerID, sessionID string) {
|
||||
rm, _ := r.rooms.Get(roomID)
|
||||
if rm == nil || rm.controller == nil || !rm.controller.DisconnectSession(peerID, sessionID) {
|
||||
@@ -948,7 +928,6 @@ func (r *router) disconnectParticipant(roomID, peerID, sessionID string) {
|
||||
r.requestPump(roomID)
|
||||
}
|
||||
|
||||
// removeViewer ports removeViewer.
|
||||
func (r *router) removeViewer(roomID, peerID string) {
|
||||
rm, _ := r.rooms.Get(roomID)
|
||||
if rm != nil {
|
||||
@@ -961,7 +940,7 @@ func (r *router) removeViewer(roomID, peerID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// stopRoom ports stopRoom: advertised capacities survive the share (O32).
|
||||
// stopRoom preserves advertised capacities across shares.
|
||||
func (r *router) stopRoom(roomID string) {
|
||||
type capacity struct {
|
||||
peerID string
|
||||
@@ -982,7 +961,6 @@ func (r *router) stopRoom(roomID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// deleteRoom ports deleteRoom.
|
||||
func (r *router) deleteRoom(roomID string) {
|
||||
r.clearRoom(roomID)
|
||||
}
|
||||
@@ -996,8 +974,7 @@ func (r *router) room(roomID string) *roomRuntime {
|
||||
return rm
|
||||
}
|
||||
|
||||
// createController ports createController (O18: connected viewers in store
|
||||
// order).
|
||||
// createController connects viewers in store order.
|
||||
func (r *router) createController(roomID string, rm *roomRuntime, host authenticatedRouteParticipant) {
|
||||
routePolicy := host.routePolicy
|
||||
operationTimeoutMs := defaultRouteOperationTimeoutMs
|
||||
@@ -1031,7 +1008,7 @@ func (r *router) createController(roomID string, rm *roomRuntime, host authentic
|
||||
}
|
||||
}
|
||||
|
||||
// clearRoom ports clearRoom (O31: admission publication order).
|
||||
// clearRoom drains publications in admission order.
|
||||
func (r *router) clearRoom(roomID string) {
|
||||
rm, ok := r.rooms.Get(roomID)
|
||||
if !ok {
|
||||
@@ -1060,9 +1037,9 @@ type assignmentEdge struct {
|
||||
publicationGeneration string
|
||||
}
|
||||
|
||||
// assignments ports assignments(). candidate overlays the current attempt
|
||||
// (nil for the committed graph only). O19: peers are the host first, then
|
||||
// the store's viewer order; O20/O21: edges keep the snapshot's insertion
|
||||
// assignments overlays the current attempt when candidate is non-nil
|
||||
// and otherwise uses only the committed graph. Peers are the host first, then
|
||||
// the store's viewer order; edges keep the snapshot's insertion
|
||||
// order, which is the childPeerIds order on the wire.
|
||||
func (r *router) assignments(
|
||||
roomID string,
|
||||
@@ -1185,8 +1162,8 @@ func (r *router) assignments(
|
||||
return assignments
|
||||
}
|
||||
|
||||
// broadcastActive ports broadcastActive (O23: host first, then viewers in
|
||||
// store order; one routesChanged at the end).
|
||||
// broadcastActive sends to the host first, then viewers in store order,
|
||||
// with one routesChanged call at the end.
|
||||
func (r *router) broadcastActive(roomID string, rm *roomRuntime) {
|
||||
if rm.controller == nil {
|
||||
return
|
||||
@@ -1232,7 +1209,6 @@ func (r *router) broadcastActive(roomID string, rm *roomRuntime) {
|
||||
}
|
||||
}
|
||||
|
||||
// pathIsPhysical ports pathIsPhysical.
|
||||
func (r *router) pathIsPhysical(roomID string, snapshot route.RouteSnapshot, childPeerID string) bool {
|
||||
rm, _ := r.rooms.Get(roomID)
|
||||
if rm == nil || rm.hostPeerID == "" {
|
||||
@@ -1263,7 +1239,7 @@ func (r *router) pathIsPhysical(roomID string, snapshot route.RouteSnapshot, chi
|
||||
return true
|
||||
}
|
||||
|
||||
// childForPair ports childForPair; "" is the TS undefined.
|
||||
// childForPair returns an empty string when neither peer is the other's child.
|
||||
func childForPair(snapshot route.RouteSnapshot, firstPeerID, secondPeerID string) string {
|
||||
if first, ok := snapshot.UpstreamByViewer.Get(firstPeerID); ok &&
|
||||
first.Kind == route.UpstreamPeer && first.ParentPeerID == secondPeerID {
|
||||
@@ -1276,7 +1252,6 @@ func childForPair(snapshot route.RouteSnapshot, firstPeerID, secondPeerID string
|
||||
return ""
|
||||
}
|
||||
|
||||
// connectedPeer ports connectedPeer.
|
||||
func (r *router) connectedPeer(roomID, peerID string) (room.ConnectedPeer, bool) {
|
||||
if host, ok := r.store.GetConnectedHost(roomID); ok && host.PeerID == peerID {
|
||||
return host, true
|
||||
@@ -1284,7 +1259,6 @@ func (r *router) connectedPeer(roomID, peerID string) (room.ConnectedPeer, bool)
|
||||
return r.store.GetConnectedViewer(roomID, peerID)
|
||||
}
|
||||
|
||||
// debugPeer ports debugPeer.
|
||||
func (r *router) debugPeer(roomID, peerID string) string {
|
||||
rm, _ := r.rooms.Get(roomID)
|
||||
if rm != nil && peerID == rm.hostPeerID {
|
||||
@@ -1332,7 +1306,7 @@ func (c qualityCopyContext) kv() []any {
|
||||
}
|
||||
}
|
||||
|
||||
// qualityCopyContext ports qualityCopyContext (O33: count only).
|
||||
// qualityCopyContext uses counts without depending on iteration order.
|
||||
func (r *router) qualityCopyContext(
|
||||
snapshot route.RouteSnapshot,
|
||||
hostPeerID string,
|
||||
@@ -1421,7 +1395,6 @@ func (r *router) qualityCopyContext(
|
||||
return context
|
||||
}
|
||||
|
||||
// debugTuple ports debugTuple.
|
||||
func (r *router) debugTuple(roomID string, tuple route.CandidateTuple) string {
|
||||
if tuple.Kind == route.UpstreamPeer {
|
||||
label := "p2p:" + r.debugPeer(roomID, tuple.ParentPeerID)
|
||||
@@ -1433,7 +1406,7 @@ func (r *router) debugTuple(roomID string, tuple route.CandidateTuple) string {
|
||||
return "sfu:" + string(tuple.Publication)
|
||||
}
|
||||
|
||||
// debug ports debug(): one sanitised slog event. details are key/value
|
||||
// debug emits one sanitised slog event. Details are key/value
|
||||
// pairs; never pass raw peer, session or connection IDs (use debugPeer).
|
||||
func (r *router) debug(roomID, event string, details ...any) {
|
||||
if !routeDebugEnabled() {
|
||||
@@ -1445,22 +1418,20 @@ func (r *router) debug(roomID, event string, details ...any) {
|
||||
slog.Info("piik-route", args...)
|
||||
}
|
||||
|
||||
// sendRoomError ports sendRoomError: only the connected host is told.
|
||||
// sendRoomError notifies only the connected host.
|
||||
func (r *router) sendRoomError(roomID, code, message string) {
|
||||
if host, ok := r.store.GetConnectedHost(roomID); ok {
|
||||
r.hooks.sendToSession(host.SessionID, protocol.ErrorMessage{Type: "error", Code: code, Message: message})
|
||||
}
|
||||
}
|
||||
|
||||
// sendViewerRouteStatus ports sendViewerRouteStatus.
|
||||
func (r *router) sendViewerRouteStatus(roomID, viewerPeerID string, message protocol.RouteStatusMessage) {
|
||||
if viewer, ok := r.store.GetConnectedViewer(roomID, viewerPeerID); ok {
|
||||
r.hooks.sendToSession(viewer.SessionID, message)
|
||||
}
|
||||
}
|
||||
|
||||
// sendRouteFailures ports sendRouteFailures (O27: deduplicated, first
|
||||
// occurrence wins).
|
||||
// sendRouteFailures deduplicates recipients; first occurrence wins.
|
||||
func (r *router) sendRouteFailures(roomID string, failedPeerIDs []string, revision int64) {
|
||||
seen := map[string]struct{}{}
|
||||
for _, peerID := range failedPeerIDs {
|
||||
@@ -1497,7 +1468,7 @@ func assignmentFor(assignments map[string]protocol.ParticipantRouteAssignment, p
|
||||
return emptyAssignment()
|
||||
}
|
||||
|
||||
// opaqueID ports opaqueId(): 16 random bytes, base64url without padding.
|
||||
// opaqueID encodes 16 random bytes as base64url without padding.
|
||||
func opaqueID() string {
|
||||
value := make([]byte, 16)
|
||||
// crypto/rand.Read never fails; it panics if the operating system source does.
|
||||
|
||||
@@ -43,25 +43,13 @@ type pumpCandidate struct {
|
||||
result prepareResult
|
||||
}
|
||||
|
||||
// requestPump ports requestPump: the single-flight driver. Every caller
|
||||
// only sets requested; the pump that owns pumping drains it.
|
||||
// requestPump starts the single-flight driver while the caller holds mu.
|
||||
// Reconcile, candidate selection and admission reservations run synchronously
|
||||
// so later calls in the handler see the result and rooms reserve SFU capacity
|
||||
// in wake order. drivePump continues the remaining work in its goroutine.
|
||||
//
|
||||
// The TS driver was an async IIFE, so it ran synchronously inside the
|
||||
// caller up to its first await: the first pumpRoom iteration through the
|
||||
// synchronous prefix of prepareCandidate (reconcile, candidate selection,
|
||||
// the admission reservations). That
|
||||
// prefix is observable: the calls that follow requestPump in the same
|
||||
// handler see its reconcile, and two rooms woken by one drain reserve
|
||||
// SFU capacity in wake order (map O29). It therefore runs here in the
|
||||
// caller's critical section; everything after it ran in microtasks and
|
||||
// runs in drivePump's goroutine.
|
||||
//
|
||||
// Deviation: `pumping` is set before that prefix, where `room.pump` was
|
||||
// still undefined until the IIFE first suspended. A re-entrant requestPump
|
||||
// during the prefix therefore only sets `requested` here and is drained by
|
||||
// drivePump's loop, instead of starting a second overlapping pump as the
|
||||
// TypeScript would have. No work is lost and the single-operation model of
|
||||
// ADR-0005 is what a second pump would have put at risk.
|
||||
// Set pumping before that synchronous prefix: re-entrant requests only set
|
||||
// requested, which drivePump drains without starting an overlapping operation.
|
||||
func (r *router) requestPump(roomID string) {
|
||||
rm, _ := r.rooms.Get(roomID)
|
||||
if rm == nil || rm.controller == nil || r.closing {
|
||||
@@ -83,8 +71,7 @@ func (r *router) requestPump(roomID string) {
|
||||
go r.drivePump(roomID, rm, controller, broadcastRevision, pending, failed)
|
||||
}
|
||||
|
||||
// recoverPump runs one pump segment; a panic is the TS rejection of the pump
|
||||
// promise (controller assertion failures panic with the TS messages, D11).
|
||||
// recoverPump reports a failed pump segment, including controller assertion panics.
|
||||
func (r *router) recoverPump(segment func()) (failed bool) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
@@ -409,9 +396,9 @@ func (r *router) prepareNewPublication(
|
||||
}}
|
||||
}
|
||||
|
||||
// sendPrepareMessages ports sendPrepareMessages (O24: fixed order). It sends
|
||||
// sendPrepareMessages preserves recipient order. It sends
|
||||
// nothing at all when the child is stale or the peer parent vanished; the
|
||||
// deadline timer then resolves the operation (hazard 7).
|
||||
// deadline timer then resolves the operation.
|
||||
func (r *router) sendPrepareMessages(
|
||||
roomID string,
|
||||
controller *route.Controller,
|
||||
@@ -459,9 +446,9 @@ func (r *router) sendPrepareMessages(
|
||||
}
|
||||
}
|
||||
|
||||
// scheduleDeadline ports scheduleDeadline (T6). The callback verifies it is
|
||||
// scheduleDeadline registers a callback that verifies it is
|
||||
// still the registered deadline through the room's generation counter, which
|
||||
// clearDeadline bumps: a cleared TS timer never fired (D5).
|
||||
// clearDeadline bumps: a cleared TS timer never fired.
|
||||
func (r *router) scheduleDeadline(roomID string, rm *roomRuntime, operation *route.OperationSnapshot) {
|
||||
r.clearDeadline(rm)
|
||||
wakeInMs := max(int64(0), operation.WakeAtMs-r.now())
|
||||
@@ -500,7 +487,7 @@ func (r *router) scheduleDeadline(roomID string, rm *roomRuntime, operation *rou
|
||||
})
|
||||
}
|
||||
|
||||
// clearDeadline ports clearDeadline; bumping the generation retires a
|
||||
// clearDeadline bumps the generation to retire a
|
||||
// callback that already lost the Stop race.
|
||||
func (r *router) clearDeadline(rm *roomRuntime) {
|
||||
if rm.deadlineStop != nil {
|
||||
@@ -521,8 +508,8 @@ func cursorGuard(operation *route.OperationSnapshot, plan route.CandidatePlan) r
|
||||
}
|
||||
}
|
||||
|
||||
// preparedRouteCandidate ports preparedRouteCandidate; connectionAttempt is
|
||||
// present only when the current attempt carries one.
|
||||
// preparedRouteCandidate includes connectionAttempt
|
||||
// only when the current attempt carries one.
|
||||
func preparedRouteCandidate(operation *route.OperationSnapshot, tuple route.CandidateTuple, connectionID string) protocol.PreparedRouteCandidate {
|
||||
transport := "sfu"
|
||||
if tuple.Kind == route.UpstreamPeer {
|
||||
|
||||
@@ -30,8 +30,7 @@ type drainOperation struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// commitReservation ports commitReservation (O30: drained generations in
|
||||
// admission order).
|
||||
// commitReservation drains generations in admission order.
|
||||
func (r *router) commitReservation(reservation route.CandidateReservation) bool {
|
||||
if reservation.Kind == route.ReservationDirect {
|
||||
return true
|
||||
@@ -76,7 +75,7 @@ func (r *router) releaseResources(resources []*route.Resource) {
|
||||
}
|
||||
}
|
||||
|
||||
// releaseResource ports releaseResource: idempotent through Released.
|
||||
// releaseResource is idempotent through Released.
|
||||
func (r *router) releaseResource(resource *route.Resource) {
|
||||
if resource.Released {
|
||||
return
|
||||
@@ -105,7 +104,7 @@ func (r *router) releaseSubscription(fence sfu.SubscriptionFence) bool {
|
||||
return accepted
|
||||
}
|
||||
|
||||
// wakeResourceWaiters ports wakeResourceWaiters (O29: snapshot order).
|
||||
// wakeResourceWaiters preserves the waiters' snapshot order.
|
||||
func (r *router) wakeResourceWaiters() {
|
||||
roomIDs := r.resourceWaiters.Keys()
|
||||
r.resourceWaiters.Clear()
|
||||
@@ -153,8 +152,8 @@ func (r *router) sendFreshSfuConfig(participant authenticatedRouteParticipant) {
|
||||
}
|
||||
}
|
||||
|
||||
// scheduleSfuPublicationDrain ports scheduleSfuPublicationDrain (O28: the
|
||||
// sibling subscription tasks are deleted during the live iteration).
|
||||
// scheduleSfuPublicationDrain deletes sibling subscription tasks during
|
||||
// the live iteration.
|
||||
func (r *router) scheduleSfuPublicationDrain(fence sfu.ResourceFence) {
|
||||
for key, task := range r.sfuDrainTasks.All() {
|
||||
if task.kind == drainSubscription && task.fence.ResourceFence == fence {
|
||||
@@ -164,7 +163,7 @@ func (r *router) scheduleSfuPublicationDrain(fence sfu.ResourceFence) {
|
||||
r.scheduleSfuDrain(drainPublication, sfu.SubscriptionFence{ResourceFence: fence})
|
||||
}
|
||||
|
||||
// scheduleSfuSubscriptionDrain ports scheduleSfuSubscriptionDrain: a pending
|
||||
// scheduleSfuSubscriptionDrain skips work when a pending
|
||||
// publication drain of the same room already removes every participant.
|
||||
func (r *router) scheduleSfuSubscriptionDrain(fence sfu.SubscriptionFence) {
|
||||
if r.sfuDrainTasks.Has(sfu.SubscriptionFence{ResourceFence: fence.ResourceFence}) {
|
||||
@@ -173,7 +172,6 @@ func (r *router) scheduleSfuSubscriptionDrain(fence sfu.SubscriptionFence) {
|
||||
r.scheduleSfuDrain(drainSubscription, fence)
|
||||
}
|
||||
|
||||
// scheduleSfuDrain ports scheduleSfuDrain.
|
||||
func (r *router) scheduleSfuDrain(kind drainKind, fence sfu.SubscriptionFence) {
|
||||
if r.sfu == nil {
|
||||
return
|
||||
@@ -243,7 +241,7 @@ func (r *router) settleSfuDrain(key sfu.SubscriptionFence, task *sfuDrainTask, e
|
||||
}
|
||||
|
||||
// awaitDrain is `await task.operation` inside close(): mu is released while
|
||||
// waiting and re-acquired before returning; ctx bounds the wait (D6).
|
||||
// waiting and re-acquired before returning; ctx bounds the wait.
|
||||
func (r *router) awaitDrain(ctx context.Context, operation *drainOperation) error {
|
||||
var err error
|
||||
r.io(func() {
|
||||
@@ -257,7 +255,7 @@ func (r *router) awaitDrain(ctx context.Context, operation *drainOperation) erro
|
||||
return err
|
||||
}
|
||||
|
||||
// reservationResources ports reservationResources: a borrowed reuse edge
|
||||
// In reservationResources, a borrowed reuse edge
|
||||
// belongs to another holder and is not released.
|
||||
func reservationResources(reservation route.CandidateReservation) []*route.Resource {
|
||||
var resources []*route.Resource
|
||||
|
||||
@@ -87,7 +87,7 @@ type Options struct {
|
||||
HeartbeatIntervalMs int
|
||||
MaxConnections int
|
||||
MaxUnauthenticatedConnections int
|
||||
// AfterFunc is the timer factory (D5); nil uses time.AfterFunc.
|
||||
// AfterFunc is the timer factory; nil uses time.AfterFunc.
|
||||
AfterFunc func(time.Duration, func()) func() bool
|
||||
// Logger receives the three console.error records; nil uses slog.Default().
|
||||
Logger *slog.Logger
|
||||
@@ -132,11 +132,10 @@ type viewerQualityEvidenceAttempt struct {
|
||||
// `undefined === undefined` comparisons meaningful.
|
||||
type roomShare struct {
|
||||
// generation survives stopSharing; only closeRoom drops the whole
|
||||
// record (hazard 2).
|
||||
// record.
|
||||
generation string
|
||||
// qualitySettings and routePolicy are nil where the TS map had no entry;
|
||||
// their presence is observable (hazard 3, and the host `authenticated`
|
||||
// quality default).
|
||||
// Missing qualitySettings and routePolicy remain nil; their presence
|
||||
// affects the host's authenticated response and quality defaults.
|
||||
qualitySettings *protocol.QualitySettings
|
||||
routePolicy *protocol.RoutePolicy
|
||||
// pausedGeneration is the share generation the Host paused, "" if none.
|
||||
@@ -180,12 +179,11 @@ type Server struct {
|
||||
|
||||
router *router
|
||||
|
||||
// sessions is socketStates (O11: heartbeat walks it in insertion order);
|
||||
// sessionsByID is socketsBySessionId.
|
||||
// Heartbeat walks sessions in insertion order; sessionsByID indexes them.
|
||||
sessions ordered.Map[*session, struct{}]
|
||||
sessionsByID map[string]*session
|
||||
// pendingConnections reserves the slots of upgrades between the capacity
|
||||
// check and accept() (D4).
|
||||
// check and accept().
|
||||
pendingConnections int
|
||||
unauthenticatedConnections int
|
||||
|
||||
@@ -329,7 +327,7 @@ func InviteURL(publicBaseURL *url.URL, roomID, viewerGrant string) string {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ServeHTTP is the "upgrade" listener: the TS rejection ladder, then the
|
||||
// accept (D4).
|
||||
// accept.
|
||||
func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
// accepts a scheme-relative target such as "//[" as a plain path, so
|
||||
// the WHATWG authority parse is re-run on the raw target.
|
||||
@@ -400,7 +398,7 @@ func rejectUpgrade(writer http.ResponseWriter, status int) {
|
||||
writer.WriteHeader(status)
|
||||
}
|
||||
|
||||
// isAllowedOrigin ports isAllowedOrigin: present, parses, equal to its own
|
||||
// isAllowedOrigin requires a present, parseable origin equal to its own
|
||||
// serialised origin (so "http://x/path" is refused) and configured.
|
||||
func (s *Server) isAllowedOrigin(origin string) bool {
|
||||
if origin == "" {
|
||||
@@ -414,8 +412,7 @@ func (s *Server) isAllowedOrigin(origin string) bool {
|
||||
return config.Origin(parsed) == origin && allowed
|
||||
}
|
||||
|
||||
// hasConnectionCapacity ports hasConnectionCapacity with the reserved slots
|
||||
// counted in.
|
||||
// hasConnectionCapacity includes reserved upgrade slots.
|
||||
func (s *Server) hasConnectionCapacity() bool {
|
||||
return !s.closing &&
|
||||
s.sessions.Len()+s.pendingConnections < s.maxConnections &&
|
||||
@@ -426,8 +423,8 @@ func (s *Server) hasConnectionCapacity() bool {
|
||||
// lifecycle and HTTP-facing mutations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Close ports close() (D6). Every client is told 1012 "Service restart"
|
||||
// before the router drains, as the TS did; readers get one second to finish
|
||||
// Close tells every client 1012 "Service restart" before the router drains.
|
||||
// Readers get one second to finish
|
||||
// the close handshake, survivors are terminated, and the router's drain
|
||||
// error, if any, is returned after everything else settled.
|
||||
func (s *Server) Close(ctx context.Context) error {
|
||||
@@ -446,23 +443,13 @@ func (s *Server) Close(ctx context.Context) error {
|
||||
sess.close(websocket.StatusServiceRestart, "Service restart")
|
||||
}
|
||||
|
||||
// T4: the grace starts where forceCloseTimer was armed, before
|
||||
// `await this.hybridMediaRouter.close()` (signaling.ts:251-259).
|
||||
//
|
||||
// Deviation (D6): the TS timer fired on its own and terminated the
|
||||
// remaining clients at one second even while the drains were still
|
||||
// running; here the grace is only consumed by the waits below, so a
|
||||
// survivor is terminated after max(1 s, the router close). D6 specifies
|
||||
// this shape ("wait for reader goroutines with min(ctx, 1 s), then
|
||||
// CloseNow survivors"), and Close returns at the same moment either way,
|
||||
// because the TS also awaited the drains before returning.
|
||||
// Start reader grace before draining the router. After the drain, wait
|
||||
// only for the remaining grace or ctx cancellation, then force-close
|
||||
// surviving readers.
|
||||
grace := time.NewTimer(serviceRestartCloseGrace)
|
||||
defer grace.Stop()
|
||||
// TS close() ran `this.closing = true` and the router's synchronous close
|
||||
// prefix (its own closing flag, every deadline and operation
|
||||
// timer, the drains) in one tick; only the drain waits yielded. Keeping mu
|
||||
// held across that prefix reproduces it: a router timer callback that
|
||||
// fires now cannot slip in between and start a new media operation.
|
||||
// Keep mu held through the router's synchronous closing prefix so a
|
||||
// timer callback cannot start a new media operation during shutdown.
|
||||
routeCloseError := s.router.close(ctx)
|
||||
s.mu.Unlock()
|
||||
|
||||
@@ -502,7 +489,7 @@ func (s *Server) Close(ctx context.Context) error {
|
||||
return routeCloseError
|
||||
}
|
||||
|
||||
// EndAllRooms ports endAllRooms() (O3: store order).
|
||||
// EndAllRooms closes rooms in store order.
|
||||
func (s *Server) EndAllRooms() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -516,11 +503,9 @@ func (s *Server) EndAllRooms() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRoomAccess ports updateRoomAccess(). The set-viewer-password action
|
||||
// releases mu around the password KDF (S3) in the sequence the room package
|
||||
// documents. Like the other room mutations it takes no context: Node ran a
|
||||
// room mutation to completion even when the browser went away, and half of a
|
||||
// mutation is worse than a wasted one.
|
||||
// UpdateRoomAccess releases mu around the password KDF for set-viewer-password,
|
||||
// following the room package's sequence. Room mutations take no context: they
|
||||
// run to completion even when the browser goes away, avoiding partial mutation.
|
||||
func (s *Server) UpdateRoomAccess(
|
||||
roomID, hostToken string,
|
||||
request protocol.RoomAccessUpdateRequest,
|
||||
@@ -592,8 +577,7 @@ func (s *Server) deriveViewerPasswordMaterial(
|
||||
})
|
||||
}
|
||||
|
||||
// ReplaceRoom ports replaceRoom() (S4): the KDF runs unlocked, the commit
|
||||
// and closeRoom run locked.
|
||||
// ReplaceRoom runs the KDF unlocked; the commit and closeRoom run locked.
|
||||
func (s *Server) ReplaceRoom(
|
||||
roomID, hostToken string,
|
||||
codeEntryPolicy protocol.CodeEntryPolicy,
|
||||
@@ -638,7 +622,7 @@ func (s *Server) CreateRoom(
|
||||
return s.store.CreateRoom(codeEntryPolicy, material, derived, preferredRoomID)
|
||||
}
|
||||
|
||||
// armHeartbeat is the heartbeat setInterval (T1): each tick re-arms the
|
||||
// armHeartbeat is the heartbeat setInterval: each tick re-arms the
|
||||
// next; a tick that lost the Stop race sees closing.
|
||||
func (s *Server) armHeartbeat() {
|
||||
interval := time.Duration(s.heartbeatIntervalMs) * time.Millisecond
|
||||
@@ -661,7 +645,7 @@ func (s *Server) armHeartbeat() {
|
||||
// messages
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// handleMessage ports handleMessage(). mu must be held.
|
||||
// handleMessage requires mu to be held.
|
||||
func (s *Server) handleMessage(sess *session, encoded []byte) {
|
||||
if !s.sessions.Has(sess) || s.closing {
|
||||
return
|
||||
@@ -765,10 +749,9 @@ func displayNameString(name *protocol.DisplayName) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
// authenticate ports authenticate() up to its one await. The non-password
|
||||
// branches had no await and run to completion here, inside the caller's
|
||||
// critical section, exactly as the TS ran them; the password branch is a
|
||||
// goroutine (D5) so the reader keeps reading while the KDF runs.
|
||||
// authenticate completes non-password checks in the caller's critical
|
||||
// section. Password checks run in a goroutine so the reader can keep reading
|
||||
// while the KDF runs.
|
||||
func (s *Server) authenticate(sess *session, request authRequest) {
|
||||
if request.role == protocol.RoleHost && !sess.siteAccessAuthenticated {
|
||||
s.sendError(sess, "AUTH_REQUIRED", "Site access is required")
|
||||
@@ -822,7 +805,7 @@ func (s *Server) finishAuthenticating(sess *session) {
|
||||
}
|
||||
}
|
||||
|
||||
// authenticateWithPassword is the connectViewerWithPassword branch (S5): the
|
||||
// authenticateWithPassword is the connectViewerWithPassword branch: the
|
||||
// KDF runs with mu released; mayConnect is evaluated on the derive goroutine
|
||||
// (gate admission) and again inside the commit, both against live state.
|
||||
func (s *Server) authenticateWithPassword(sess *session, request authRequest) {
|
||||
@@ -875,12 +858,11 @@ func (s *Server) authenticationFailed(sess *session, request authRequest, err er
|
||||
sess.close(websocket.StatusCode(protocol.SignalCloseAuthenticationFailed), "Authentication failed")
|
||||
}
|
||||
|
||||
// completeAuthentication is authenticate() after the store admitted the
|
||||
// participant (lines 519-751), starting with the post-await guard.
|
||||
// completeAuthentication rechecks session ownership after store admission.
|
||||
func (s *Server) completeAuthentication(sess *session, request authRequest, participant room.ConnectedParticipant) {
|
||||
if s.closing || !s.sessions.Has(sess) || sess.revoked || !sess.open() {
|
||||
if _, err := s.store.DisconnectParticipant(participant.RoomID, participant.PeerID, sess.sessionID); err != nil {
|
||||
// The TS threw out of the async function; there is no catcher (D7).
|
||||
// The TS threw out of the async function; there is no catcher.
|
||||
panic(fmt.Errorf("room store failed while releasing an aborted authentication: %w", err))
|
||||
}
|
||||
return
|
||||
@@ -933,7 +915,7 @@ func (s *Server) completeAuthentication(sess *session, request authRequest, part
|
||||
connectionID = ¤t
|
||||
}
|
||||
}
|
||||
// O7: the host-status fan-out below uses this pre-router snapshot.
|
||||
// The host-status fan-out below uses this pre-router snapshot.
|
||||
var connectedViewers []room.ConnectedPeer
|
||||
if participant.Role == protocol.RoleHost {
|
||||
connectedViewers = s.store.GetConnectedViewers(roomID)
|
||||
@@ -1057,7 +1039,7 @@ func (s *Server) settleHostShare(roomID, sessionID string, request authRequest)
|
||||
initialQualitySettings = *request.qualitySettings
|
||||
}
|
||||
share.qualitySettings = &initialQualitySettings
|
||||
// O6: store order.
|
||||
// Store order.
|
||||
for _, viewer := range s.store.GetConnectedViewers(roomID) {
|
||||
s.sendToSession(viewer.SessionID, protocol.QualitySettingsMessage{
|
||||
Type: "quality-settings",
|
||||
@@ -1074,7 +1056,7 @@ func (s *Server) settleHostShare(roomID, sessionID string, request authRequest)
|
||||
ShareGeneration: shareGeneration,
|
||||
RoutePolicy: routePolicy,
|
||||
}
|
||||
// Hazard 3: no `has(roomId)` term here, unlike `authenticated`.
|
||||
// No `has(roomId)` term here, unlike `authenticated`.
|
||||
s.sendToSession(viewer.SessionID, message)
|
||||
}
|
||||
}
|
||||
@@ -1106,7 +1088,7 @@ func authenticatedDisplayName(request authRequest) *string {
|
||||
|
||||
// connectRouteParticipant is the try/catch around
|
||||
// hybridMediaRouter.connectParticipant: a controller assertion panics with
|
||||
// the TS message (D11), which the TS catch swallowed.
|
||||
// the TS message, which the TS catch swallowed.
|
||||
func (s *Server) connectRouteParticipant(participant authenticatedRouteParticipant) (state hybridAuthenticationState, assigned bool) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
@@ -1141,7 +1123,6 @@ func routeParticipant(sess *session, authenticated *authenticatedSession) authen
|
||||
}
|
||||
}
|
||||
|
||||
// handleAuthenticatedMessage ports handleAuthenticatedMessage().
|
||||
func (s *Server) handleAuthenticatedMessage(sess *session, authenticated *authenticatedSession, message protocol.ClientMessage) {
|
||||
switch m := message.(type) {
|
||||
case protocol.SignalingChallengeMessage:
|
||||
@@ -1181,7 +1162,7 @@ func (s *Server) handleAuthenticatedMessage(sess *session, authenticated *authen
|
||||
share := s.shares[authenticated.roomID]
|
||||
share.qualitySettings = &m.QualitySettings
|
||||
s.shares[authenticated.roomID] = share
|
||||
// O8: store order.
|
||||
// Store order.
|
||||
for _, viewer := range s.store.GetConnectedViewers(authenticated.roomID) {
|
||||
s.sendToSession(viewer.SessionID, protocol.QualitySettingsMessage{
|
||||
Type: "quality-settings",
|
||||
@@ -1270,11 +1251,11 @@ func (s *Server) handleAuthenticatedMessage(sess *session, authenticated *authen
|
||||
sess.close(websocket.StatusCode(protocol.SignalCloseSessionReplaced), "Sharing generation replaced")
|
||||
return
|
||||
}
|
||||
// Hazard 4: the participant leaves the store before stopSharing()
|
||||
// The participant leaves the store before stopSharing()
|
||||
// so the sharing-stopped / host-status pair is what viewers see.
|
||||
if s.sessions.Has(sess) && sess.authenticated == authenticated {
|
||||
if _, err := s.store.DisconnectParticipant(authenticated.roomID, authenticated.peerID, sess.sessionID); err != nil {
|
||||
// The TS threw out of the message handler; nothing caught it (D7).
|
||||
// The TS threw out of the message handler; nothing caught it.
|
||||
panic(fmt.Errorf("room store failed while stopping sharing: %w", err))
|
||||
}
|
||||
}
|
||||
@@ -1297,9 +1278,8 @@ func (s *Server) handleAuthenticatedMessage(sess *session, authenticated *authen
|
||||
// quality evidence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// handleViewerQualityEvidence ports handleViewerQualityEvidence(). The
|
||||
// forwarded envelope is encoded once; the same bytes reach the upstream
|
||||
// recipient and, when different and opted in, the host (hazard 10).
|
||||
// handleViewerQualityEvidence encodes the forwarded envelope once; the same bytes reach the upstream
|
||||
// recipient and, when different and opted in, the host.
|
||||
func (s *Server) handleViewerQualityEvidence(sess *session, source *authenticatedSession, message protocol.ViewerQualityEvidenceMessage) {
|
||||
if source.role != protocol.RoleViewer {
|
||||
return
|
||||
@@ -1429,7 +1409,6 @@ func (s *Server) authenticatedOf(sessionID string) *authenticatedSession {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSenderQualityEvidence ports handleSenderQualityEvidence().
|
||||
func (s *Server) handleSenderQualityEvidence(sess *session, source *authenticatedSession, message protocol.SenderQualityEvidenceMessage) {
|
||||
if source.peerID == message.ChildPeerID {
|
||||
return
|
||||
@@ -1443,7 +1422,6 @@ func (s *Server) handleSenderQualityEvidence(sess *session, source *authenticate
|
||||
s.router.observeSenderQualityEvidence(routeParticipant(sess, source), message)
|
||||
}
|
||||
|
||||
// handleSfuPublisherQualityEvidence ports handleSfuPublisherQualityEvidence().
|
||||
func (s *Server) handleSfuPublisherQualityEvidence(sess *session, source *authenticatedSession, message protocol.SfuPublisherQualityEvidenceMessage) {
|
||||
if source.role != protocol.RoleHost {
|
||||
return
|
||||
@@ -1457,7 +1435,6 @@ func (s *Server) handleSfuPublisherQualityEvidence(sess *session, source *authen
|
||||
s.router.observeSfuPublisherQualityEvidence(routeParticipant(sess, source), message)
|
||||
}
|
||||
|
||||
// consumeSenderQualityBudget ports consumeSenderQualityBudget().
|
||||
func (s *Server) consumeSenderQualityBudget(sessionID string, nowMs int64) bool {
|
||||
maximumReports := min(s.endpointMediaCopyCapacity+1, 3) * 2
|
||||
current := s.senderQualityRateBySession[sessionID]
|
||||
@@ -1476,7 +1453,6 @@ func (s *Server) consumeSenderQualityBudget(sessionID string, nowMs int64) bool
|
||||
// peer-assisted signaling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// routePeerAssistedSignal ports routeSignal / routePeerAssistedSignal.
|
||||
func (s *Server) routePeerAssistedSignal(sess *session, source *authenticatedSession, message protocol.ClientSignalMessage) {
|
||||
targetPeerID := message.TargetPeerID
|
||||
if targetPeerID == "" {
|
||||
@@ -1557,7 +1533,6 @@ func (s *Server) routePeerAssistedSignal(sess *session, source *authenticatedSes
|
||||
})
|
||||
}
|
||||
|
||||
// routePeerAssistedRestart ports routePeerAssistedRestart.
|
||||
func (s *Server) routePeerAssistedRestart(sess *session, source *authenticatedSession, message protocol.ClientRestartRequestMessage) {
|
||||
targetPeerID := message.TargetPeerID
|
||||
if targetPeerID == "" || !s.router.isActivePeerParentOf(source.roomID, targetPeerID, source.peerID) {
|
||||
@@ -1577,7 +1552,7 @@ func (s *Server) routePeerAssistedRestart(sess *session, source *authenticatedSe
|
||||
})
|
||||
}
|
||||
|
||||
// isCurrentSession ports isCurrentSession: checked on every authenticated
|
||||
// isCurrentSession is checked on every authenticated
|
||||
// message before dispatch.
|
||||
func (s *Server) isCurrentSession(sess *session) bool {
|
||||
authenticated := sess.authenticated
|
||||
@@ -1594,7 +1569,6 @@ func (s *Server) isCurrentSession(sess *session) bool {
|
||||
return ok && current.SessionID == sess.sessionID
|
||||
}
|
||||
|
||||
// connectedPeer ports connectedPeer.
|
||||
func (s *Server) connectedPeer(roomID, peerID string) (room.ConnectedPeer, bool) {
|
||||
if host, ok := s.store.GetConnectedHost(roomID); ok && host.PeerID == peerID {
|
||||
return host, true
|
||||
@@ -1606,7 +1580,6 @@ func (s *Server) connectedPeer(roomID, peerID string) (room.ConnectedPeer, bool)
|
||||
// error tables
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// authenticationErrorMessage ports authenticationErrorMessage.
|
||||
func authenticationErrorMessage(code string) string {
|
||||
switch code {
|
||||
case "ROOM_ACCESS_DENIED":
|
||||
@@ -1623,7 +1596,6 @@ func authenticationErrorMessage(code string) string {
|
||||
return "Authentication failed"
|
||||
}
|
||||
|
||||
// authenticationErrorCode ports authenticationErrorCode verbatim.
|
||||
func authenticationErrorCode(request authRequest, err error) string {
|
||||
var roomError *room.Error
|
||||
if !errors.As(err, &roomError) || roomError.Code == room.CodeRoomLimit {
|
||||
|
||||
@@ -54,9 +54,9 @@ type session struct {
|
||||
lastSignalingChallengeAtMs int64
|
||||
authenticated *authenticatedSession
|
||||
|
||||
// authStop stops the authentication deadline (T3); authGeneration is
|
||||
// authStop stops the authentication deadline; authGeneration is
|
||||
// bumped whenever the TS cleared the timer so a callback that lost the
|
||||
// Stop race sees it is stale (D5).
|
||||
// Stop race sees it is stale.
|
||||
authStop func() bool
|
||||
authGeneration uint64
|
||||
|
||||
@@ -135,8 +135,7 @@ func (sess *session) clearAuthenticationTimer() {
|
||||
sess.authGeneration++
|
||||
}
|
||||
|
||||
// accept ports accept(): the slot is counted here, the two goroutines start
|
||||
// here. mu must be held.
|
||||
// accept counts the slot and starts both goroutines. mu must be held.
|
||||
func (s *Server) accept(conn *websocket.Conn, siteAccessAuthenticated bool) *session {
|
||||
if !s.hasConnectionCapacity() {
|
||||
go func() { _ = conn.CloseNow() }()
|
||||
@@ -152,7 +151,7 @@ func (s *Server) accept(conn *websocket.Conn, siteAccessAuthenticated bool) *ses
|
||||
return sess
|
||||
}
|
||||
|
||||
// armAuthenticationTimer is the setTimeout of accept() (T3). The TS timer
|
||||
// armAuthenticationTimer is the setTimeout of accept(). The TS timer
|
||||
// fired blind; the generation check stands in for clearTimeout.
|
||||
func (s *Server) armAuthenticationTimer(sess *session) {
|
||||
generation := sess.authGeneration
|
||||
@@ -169,7 +168,7 @@ func (s *Server) armAuthenticationTimer(sess *session) {
|
||||
}
|
||||
|
||||
// readLoop is the "message" handler plus the "close" handler: it reads until
|
||||
// the connection fails and then runs handleDisconnect exactly once (D6).
|
||||
// the connection fails and then runs handleDisconnect exactly once.
|
||||
func (s *Server) readLoop(sess *session) {
|
||||
defer func() {
|
||||
defer close(sess.readerDone)
|
||||
@@ -198,7 +197,7 @@ func (s *Server) handleIncoming(sess *session, kind websocket.MessageType, data
|
||||
}
|
||||
|
||||
// writeLoop drains the outbound queue. Conn.Write and Conn.Close are only
|
||||
// ever called here, never under mu (D4).
|
||||
// ever called here, never under mu.
|
||||
func (s *Server) writeLoop(sess *session) {
|
||||
for {
|
||||
s.mu.Lock()
|
||||
@@ -240,8 +239,8 @@ func (sess *session) ping(timeout time.Duration) {
|
||||
_ = sess.conn.Ping(ctx)
|
||||
}
|
||||
|
||||
// heartbeat ports heartbeat() (O11: insertion order). It returns the
|
||||
// sessions to ping so the caller can do that after releasing mu.
|
||||
// heartbeat returns sessions in insertion order so the caller can ping
|
||||
// them after releasing mu.
|
||||
func (s *Server) heartbeat() []*session {
|
||||
var pings []*session
|
||||
for sess := range s.sessions.All() {
|
||||
@@ -255,8 +254,8 @@ func (s *Server) heartbeat() []*session {
|
||||
return pings
|
||||
}
|
||||
|
||||
// send ports send(): drop unless open, terminate above the buffer bound,
|
||||
// otherwise queue the encoded message.
|
||||
// send drops unless open, terminates above the buffer bound,
|
||||
// and otherwise queues the encoded message.
|
||||
func (s *Server) send(sess *session, message protocol.ServerMessage) {
|
||||
encoded, err := protocol.EncodeServerMessage(message)
|
||||
if err != nil {
|
||||
@@ -278,15 +277,13 @@ func (s *Server) sendEncoded(sess *session, encoded []byte) {
|
||||
sess.enqueue(encoded)
|
||||
}
|
||||
|
||||
// sendToSession ports sendToSession().
|
||||
func (s *Server) sendToSession(sessionID string, message protocol.ServerMessage) {
|
||||
if sess := s.sessionsByID[sessionID]; sess != nil {
|
||||
s.send(sess, message)
|
||||
}
|
||||
}
|
||||
|
||||
// sendEncodedToSession ports sendEncodedToSession(): the same bytes go to
|
||||
// every recipient (hazard 10).
|
||||
// sendEncodedToSession reuses the same encoded bytes for every recipient.
|
||||
func (s *Server) sendEncodedToSession(sessionID string, encoded []byte) {
|
||||
if sess := s.sessionsByID[sessionID]; sess != nil {
|
||||
s.sendEncoded(sess, encoded)
|
||||
@@ -297,7 +294,6 @@ func (s *Server) sendError(sess *session, code, message string) {
|
||||
s.send(sess, protocol.ErrorMessage{Type: "error", Code: code, Message: message})
|
||||
}
|
||||
|
||||
// rejectInvalidMessage ports rejectInvalidMessage().
|
||||
func (s *Server) rejectInvalidMessage(sess *session) {
|
||||
s.sendError(sess, "INVALID_MESSAGE", "Message is invalid")
|
||||
sess.close(websocket.StatusPolicyViolation, "Invalid message")
|
||||
|
||||
Reference in new issue
Block a user